/*
* Copyright 2013-2017 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.save;
import static com.keepassdroid.database.PwDatabaseV4XML.*;
import java.io.IOException;
import java.io.OutputStream;
import java.security.SecureRandom;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Stack;
import java.util.UUID;
import java.util.zip.GZIPOutputStream;
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import org.spongycastle.crypto.StreamCipher;
import org.xmlpull.v1.XmlSerializer;
import android.util.Xml;
import biz.source_code.base64Coder.Base64Coder;
import com.keepassdroid.crypto.CipherFactory;
import com.keepassdroid.crypto.PwStreamCipherFactory;
import com.keepassdroid.crypto.engine.CipherEngine;
import com.keepassdroid.crypto.keyDerivation.KdfEngine;
import com.keepassdroid.crypto.keyDerivation.KdfFactory;
import com.keepassdroid.database.BinaryPool;
import com.keepassdroid.database.CrsAlgorithm;
import com.keepassdroid.database.EntryHandler;
import com.keepassdroid.database.GroupHandler;
import com.keepassdroid.database.ITimeLogger;
import com.keepassdroid.database.PwCompressionAlgorithm;
import com.keepassdroid.database.PwDatabaseV4;
import com.keepassdroid.database.PwDatabaseV4.MemoryProtectionConfig;
import com.keepassdroid.database.PwDatabaseV4XML;
import com.keepassdroid.database.PwDbHeader;
import com.keepassdroid.database.PwDbHeaderV4;
import com.keepassdroid.database.PwDefsV4;
import com.keepassdroid.database.PwDeletedObject;
import com.keepassdroid.database.PwEntry;
import com.keepassdroid.database.PwEntryV4;
import com.keepassdroid.database.PwEntryV4.AutoType;
import com.keepassdroid.database.PwGroup;
import com.keepassdroid.database.PwGroupV4;
import com.keepassdroid.database.PwIconCustom;
import com.keepassdroid.database.exception.PwDbOutputException;
import com.keepassdroid.database.security.ProtectedBinary;
import com.keepassdroid.database.security.ProtectedString;
import com.keepassdroid.stream.HashedBlockOutputStream;
import com.keepassdroid.utils.EmptyUtils;
import com.keepassdroid.utils.MemUtil;
import com.keepassdroid.utils.Types;
public class PwDbV4Output extends PwDbOutput {
PwDatabaseV4 mPM;
private StreamCipher randomStream;
private BinaryPool binPool;
private XmlSerializer xml;
private PwDbHeaderV4 header;
private byte[] hashOfHeader;
protected PwDbV4Output(PwDatabaseV4 pm, OutputStream os) {
super(os);
mPM = pm;
}
@Override
public void output() throws PwDbOutputException {
header = (PwDbHeaderV4) outputHeader(mOS);
CipherOutputStream cos = attachStreamEncryptor(header, mOS);
OutputStream compressed;
try {
cos.write(header.streamStartBytes);
HashedBlockOutputStream hashed = new HashedBlockOutputStream(cos);
if ( mPM.compressionAlgorithm == PwCompressionAlgorithm.Gzip ) {
compressed = new GZIPOutputStream(hashed);
} else {
compressed = hashed;
}
outputDatabase(compressed);
compressed.close();
} catch (IllegalArgumentException e) {
throw new PwDbOutputException(e);
} catch (IllegalStateException e) {
throw new PwDbOutputException(e);
} catch (IOException e) {
throw new PwDbOutputException(e);
}
}
private class GroupWriter extends GroupHandler<PwGroup> {
private Stack<PwGroupV4> groupStack;
public GroupWriter(Stack<PwGroupV4> gs) {
groupStack = gs;
}
@Override
public boolean operate(PwGroup g) {
PwGroupV4 group = (PwGroupV4) g;
assert(group != null);
while(true) {
try {
if (group.parent == groupStack.peek()) {
groupStack.push(group);
startGroup(group);
break;
} else {
groupStack.pop();
if (groupStack.size() <= 0) return false;
endGroup();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return true;
}
}
private class EntryWriter extends EntryHandler<PwEntry> {
@Override
public boolean operate(PwEntry e) {
PwEntryV4 entry = (PwEntryV4) e;
assert(entry != null);
try {
writeEntry(entry, false);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
return true;
}
}
private void outputDatabase(OutputStream os) throws IllegalArgumentException, IllegalStateException, IOException {
binPool = new BinaryPool((PwGroupV4)mPM.rootGroup);
xml = Xml.newSerializer();
xml.setOutput(os, "UTF-8");
xml.startDocument("UTF-8", true);
xml.startTag(null, ElemDocNode);
writeMeta();
PwGroupV4 root = (PwGroupV4) mPM.rootGroup;
xml.startTag(null, ElemRoot);
startGroup(root);
Stack<PwGroupV4> groupStack = new Stack<PwGroupV4>();
groupStack.push(root);
if (!root.preOrderTraverseTree(new GroupWriter(groupStack), new EntryWriter())) throw new RuntimeException("Writing groups failed");
while (groupStack.size() > 1) {
xml.endTag(null, ElemGroup);
groupStack.pop();
}
endGroup();
writeList(ElemDeletedObjects, mPM.deletedObjects);
xml.endTag(null, ElemRoot);
xml.endTag(null, ElemDocNode);
xml.endDocument();
}
private void writeMeta() throws IllegalArgumentException, IllegalStateException, IOException {
xml.startTag(null, ElemMeta);
writeObject(ElemGenerator, mPM.localizedAppName);
if (hashOfHeader != null) {
writeObject(ElemHeaderHash, String.valueOf(Base64Coder.encode(hashOfHeader)));
}
writeObject(ElemDbName, mPM.name, true);
writeObject(ElemDbNameChanged, mPM.nameChanged);
writeObject(ElemDbDesc, mPM.description, true);
writeObject(ElemDbDescChanged, mPM.descriptionChanged);
writeObject(ElemDbDefaultUser, mPM.defaultUserName, true);
writeObject(ElemDbDefaultUserChanged, mPM.defaultUserNameChanged);
writeObject(ElemDbMntncHistoryDays, mPM.maintenanceHistoryDays);
writeObject(ElemDbColor, mPM.color);
writeObject(ElemDbKeyChanged, mPM.keyLastChanged);
writeObject(ElemDbKeyChangeRec, mPM.keyChangeRecDays);
writeObject(ElemDbKeyChangeForce, mPM.keyChangeForceDays);
writeList(ElemMemoryProt, mPM.memoryProtection);
writeCustomIconList();
writeObject(ElemRecycleBinEnabled, mPM.recycleBinEnabled);
writeObject(ElemRecycleBinUuid, mPM.recycleBinUUID);
writeObject(ElemRecycleBinChanged, mPM.recycleBinChanged);
writeObject(ElemEntryTemplatesGroup, mPM.entryTemplatesGroup);
writeObject(ElemEntryTemplatesGroupChanged, mPM.entryTemplatesGroupChanged);
writeObject(ElemHistoryMaxItems, mPM.historyMaxItems);
writeObject(ElemHistoryMaxSize, mPM.historyMaxSize);
writeObject(ElemLastSelectedGroup, mPM.lastSelectedGroup);
writeObject(ElemLastTopVisibleGroup, mPM.lastTopVisibleGroup);
writeBinPool();
writeList(ElemCustomData, mPM.customData);
xml.endTag(null, ElemMeta);
}
private CipherOutputStream attachStreamEncryptor(PwDbHeaderV4 header, OutputStream os) throws PwDbOutputException {
Cipher cipher;
CipherEngine engine;
try {
mPM.makeFinalKey(header.masterSeed, header.getTransformSeed(), (int)mPM.numKeyEncRounds);
engine = CipherFactory.getInstance(mPM.dataCipher);
cipher = engine.getCipher(Cipher.ENCRYPT_MODE, mPM.finalKey, header.encryptionIV);
} catch (Exception e) {
throw new PwDbOutputException("Invalid algorithm.", e);
}
CipherOutputStream cos = new CipherOutputStream(os, cipher);
return cos;
}
@Override
protected SecureRandom setIVs(PwDbHeader header) throws PwDbOutputException {
SecureRandom random = super.setIVs(header);
PwDbHeaderV4 h = (PwDbHeaderV4) header;
random.nextBytes(h.masterSeed);
random.nextBytes(h.encryptionIV);
UUID kdfUUID = mPM.kdfParameters.kdfUUID;
KdfEngine kdf = KdfFactory.get(kdfUUID);
kdf.randomize(mPM.kdfParameters);
random.nextBytes(h.protectedStreamKey);
h.innerRandomStream = CrsAlgorithm.Salsa20;
randomStream = PwStreamCipherFactory.getInstance(h.innerRandomStream, h.protectedStreamKey);
if (randomStream == null) {
throw new PwDbOutputException("Invalid random cipher");
}
random.nextBytes(h.streamStartBytes);
return random;
}
@Override
public PwDbHeader outputHeader(OutputStream os) throws PwDbOutputException {
PwDbHeaderV4 header = new PwDbHeaderV4(mPM);
setIVs(header);
PwDbHeaderOutputV4 pho = new PwDbHeaderOutputV4(mPM, header, os);
try {
pho.output();
} catch (IOException e) {
throw new PwDbOutputException("Failed to output the header.", e);
}
hashOfHeader = pho.getHashOfHeader();
return header;
}
private void startGroup(PwGroupV4 group) throws IllegalArgumentException, IllegalStateException, IOException {
xml.startTag(null, ElemGroup);
writeObject(ElemUuid, group.uuid);
writeObject(ElemName, group.name);
writeObject(ElemNotes, group.notes);
writeObject(ElemIcon, group.icon.iconId);
if (!group.customIcon.equals(PwIconCustom.ZERO)) {
writeObject(ElemCustomIconID, group.customIcon.uuid);
}
writeList(ElemTimes, group);
writeObject(ElemIsExpanded, group.isExpanded);
writeObject(ElemGroupDefaultAutoTypeSeq, group.defaultAutoTypeSequence);
writeObject(ElemEnableAutoType, group.enableAutoType);
writeObject(ElemEnableSearching, group.enableSearching);
writeObject(ElemLastTopVisibleEntry, group.lastTopVisibleEntry);
}
private void endGroup() throws IllegalArgumentException, IllegalStateException, IOException {
xml.endTag(null, ElemGroup);
}
private void writeEntry(PwEntryV4 entry, boolean isHistory) throws IllegalArgumentException, IllegalStateException, IOException {
assert(entry != null);
xml.startTag(null, ElemEntry);
writeObject(ElemUuid, entry.uuid);
writeObject(ElemIcon, entry.icon.iconId);
if (!entry.customIcon.equals(PwIconCustom.ZERO)) {
writeObject(ElemCustomIconID, entry.customIcon.uuid);
}
writeObject(ElemFgColor, entry.foregroundColor);
writeObject(ElemBgColor, entry.backgroupColor);
writeObject(ElemOverrideUrl, entry.overrideURL);
writeObject(ElemTags, entry.tags);
writeList(ElemTimes, entry);
writeList(entry.strings, true);
writeList(entry.binaries);
writeList(ElemAutoType, entry.autoType);
if (!isHistory) {
writeList(ElemHistory, entry.history, true);
} else {
assert(entry.history.size() == 0);
}
xml.endTag(null, ElemEntry);
}
private void writeObject(String key, ProtectedBinary value, boolean allowRef) throws IllegalArgumentException, IllegalStateException, IOException {
assert(key != null && value != null);
xml.startTag(null, ElemBinary);
xml.startTag(null, ElemKey);
xml.text(safeXmlString(key));
xml.endTag(null, ElemKey);
xml.startTag(null, ElemValue);
String strRef = null;
if (allowRef) {
int ref = binPool.poolFind(value);
strRef = Integer.toString(ref);
}
if (strRef != null) {
xml.attribute(null, AttrRef, strRef);
}
else {
subWriteValue(value);
}
xml.endTag(null, ElemValue);
xml.endTag(null, ElemBinary);
}
private void subWriteValue(ProtectedBinary value) throws IllegalArgumentException, IllegalStateException, IOException {
if (value.isProtected()) {
xml.attribute(null, AttrProtected, ValTrue);
int valLength = value.length();
if (valLength > 0) {
byte[] encoded = new byte[valLength];
randomStream.processBytes(value.getData(), 0, valLength, encoded, 0);
xml.text(String.valueOf(Base64Coder.encode(encoded)));
}
} else {
if (mPM.compressionAlgorithm == PwCompressionAlgorithm.Gzip) {
xml.attribute(null, AttrCompressed, ValTrue);
byte[] raw = value.getData();
byte[] compressed = MemUtil.compress(raw);
xml.text(String.valueOf(Base64Coder.encode(compressed)));
} else {
byte[] raw = value.getData();
xml.text(String.valueOf(Base64Coder.encode(raw)));
}
}
}
private void writeObject(String name, String value, boolean filterXmlChars) throws IllegalArgumentException, IllegalStateException, IOException {
assert(name != null && value != null);
xml.startTag(null, name);
if (filterXmlChars) {
value = safeXmlString(value);
}
xml.text(value);
xml.endTag(null, name);
}
private void writeObject(String name, String value) throws IllegalArgumentException, IllegalStateException, IOException {
writeObject(name, value, false);
}
private void writeObject(String name, Date value) throws IllegalArgumentException, IllegalStateException, IOException {
writeObject(name, PwDatabaseV4XML.dateFormat.format(value));
}
private void writeObject(String name, long value) throws IllegalArgumentException, IllegalStateException, IOException {
writeObject(name, String.valueOf(value));
}
private void writeObject(String name, Boolean value) throws IllegalArgumentException, IllegalStateException, IOException {
String text;
if (value == null) {
text = "null";
}
else if (value) {
text = ValTrue;
}
else {
text = ValFalse;
}
writeObject(name, text);
}
private void writeObject(String name, UUID uuid) throws IllegalArgumentException, IllegalStateException, IOException {
byte[] data = Types.UUIDtoBytes(uuid);
writeObject(name, String.valueOf(Base64Coder.encode(data)));
}
private void writeObject(String name, String keyName, String keyValue, String valueName, String valueValue) throws IllegalArgumentException, IllegalStateException, IOException {
xml.startTag(null, name);
xml.startTag(null, keyName);
xml.text(safeXmlString(keyValue));
xml.endTag(null, keyName);
xml.startTag(null, valueName);
xml.text(safeXmlString(valueValue));
xml.endTag(null, valueName);
xml.endTag(null, name);
}
private void writeList(String name, AutoType autoType) throws IllegalArgumentException, IllegalStateException, IOException {
assert(name != null && autoType != null);
xml.startTag(null, name);
writeObject(ElemAutoTypeEnabled, autoType.enabled);
writeObject(ElemAutoTypeObfuscation, autoType.obfuscationOptions);
if (autoType.defaultSequence.length() > 0) {
writeObject(ElemAutoTypeDefaultSeq, autoType.defaultSequence, true);
}
for (Entry<String, String> pair : autoType.entrySet()) {
writeObject(ElemAutoTypeItem, ElemWindow, pair.getKey(), ElemKeystrokeSequence, pair.getValue());
}
xml.endTag(null, name);
}
private void writeList(Map<String, ProtectedString> strings, boolean isEntryString) throws IllegalArgumentException, IllegalStateException, IOException {
assert (strings != null);
for (Entry<String, ProtectedString> pair : strings.entrySet()) {
writeObject(pair.getKey(), pair.getValue(), isEntryString);
}
}
private void writeObject(String key, ProtectedString value, boolean isEntryString) throws IllegalArgumentException, IllegalStateException, IOException {
assert(key !=null && value != null);
xml.startTag(null, ElemString);
xml.startTag(null, ElemKey);
xml.text(safeXmlString(key));
xml.endTag(null, ElemKey);
xml.startTag(null, ElemValue);
boolean protect = value.isProtected();
if (isEntryString) {
if (key.equals(PwDefsV4.TITLE_FIELD)) {
protect = mPM.memoryProtection.protectTitle;
}
else if (key.equals(PwDefsV4.USERNAME_FIELD)) {
protect = mPM.memoryProtection.protectUserName;
}
else if (key.equals(PwDefsV4.PASSWORD_FIELD)) {
protect = mPM.memoryProtection.protectPassword;
}
else if (key.equals(PwDefsV4.URL_FIELD)) {
protect = mPM.memoryProtection.protectUrl;
}
else if (key.equals(PwDefsV4.NOTES_FIELD)) {
protect = mPM.memoryProtection.protectNotes;
}
}
if (protect) {
xml.attribute(null, AttrProtected, ValTrue);
byte[] data = value.toString().getBytes("UTF-8");
int valLength = data.length;
if (valLength > 0) {
byte[] encoded = new byte[valLength];
randomStream.processBytes(data, 0, valLength, encoded, 0);
xml.text(String.valueOf(Base64Coder.encode(encoded)));
}
}
else {
xml.text(safeXmlString(value.toString()));
}
xml.endTag(null, ElemValue);
xml.endTag(null, ElemString);
}
private void writeObject(String name, PwDeletedObject value) throws IllegalArgumentException, IllegalStateException, IOException {
assert(name != null && value != null);
xml.startTag(null, name);
writeObject(ElemUuid, value.uuid);
writeObject(ElemDeletionTime, value.getDeletionTime());
xml.endTag(null, name);
}
private void writeList(Map<String, ProtectedBinary> binaries) throws IllegalArgumentException, IllegalStateException, IOException {
assert(binaries != null);
for (Entry<String, ProtectedBinary> pair : binaries.entrySet()) {
writeObject(pair.getKey(), pair.getValue(), true);
}
}
private void writeList(String name, List<PwDeletedObject> value) throws IllegalArgumentException, IllegalStateException, IOException {
assert(name != null && value != null);
xml.startTag(null, name);
for (PwDeletedObject pdo : value) {
writeObject(ElemDeletedObject, pdo);
}
xml.endTag(null, name);
}
private void writeList(String name, MemoryProtectionConfig value) throws IllegalArgumentException, IllegalStateException, IOException {
assert(name != null && value != null);
xml.startTag(null, name);
writeObject(ElemProtTitle, value.protectTitle);
writeObject(ElemProtUserName, value.protectUserName);
writeObject(ElemProtPassword, value.protectPassword);
writeObject(ElemProtURL, value.protectUrl);
writeObject(ElemProtNotes, value.protectNotes);
xml.endTag(null, name);
}
private void writeList(String name, Map<String, String> customData) throws IllegalArgumentException, IllegalStateException, IOException {
assert(name != null && customData != null);
xml.startTag(null, name);
for (Entry<String, String> pair : customData.entrySet()) {
writeObject(ElemStringDictExItem, ElemKey, pair.getKey(), ElemValue, pair.getValue());
}
xml.endTag(null, name);
}
private void writeList(String name, ITimeLogger it) throws IllegalArgumentException, IllegalStateException, IOException {
assert(name != null && it != null);
xml.startTag(null, name);
writeObject(ElemLastModTime, it.getLastModificationTime());
writeObject(ElemCreationTime, it.getCreationTime());
writeObject(ElemLastAccessTime, it.getLastAccessTime());
writeObject(ElemExpiryTime, it.getExpiryTime());
writeObject(ElemExpires, it.expires());
writeObject(ElemUsageCount, it.getUsageCount());
writeObject(ElemLocationChanged, it.getLocationChanged());
xml.endTag(null, name);
}
private void writeList(String name, List<PwEntryV4> value, boolean isHistory) throws IllegalArgumentException, IllegalStateException, IOException {
assert(name != null && value != null);
xml.startTag(null, name);
for (PwEntryV4 entry : value) {
writeEntry(entry, isHistory);
}
xml.endTag(null, name);
}
private void writeCustomIconList() throws IllegalArgumentException, IllegalStateException, IOException {
List<PwIconCustom> customIcons = mPM.customIcons;
if (customIcons.size() == 0) return;
xml.startTag(null, ElemCustomIcons);
for (PwIconCustom icon : customIcons) {
xml.startTag(null, ElemCustomIconItem);
writeObject(ElemCustomIconItemID, icon.uuid);
writeObject(ElemCustomIconItemData, String.valueOf(Base64Coder.encode(icon.imageData)));
xml.endTag(null, ElemCustomIconItem);
}
xml.endTag(null, ElemCustomIcons);
}
private void writeBinPool() throws IllegalArgumentException, IllegalStateException, IOException {
xml.startTag(null, ElemBinaries);
for (Entry<Integer, ProtectedBinary> pair : binPool.entrySet()) {
xml.startTag(null, ElemBinary);
xml.attribute(null, AttrId, Integer.toString(pair.getKey()));
subWriteValue(pair.getValue());
xml.endTag(null, ElemBinary);
}
xml.endTag(null, ElemBinaries);
}
private String safeXmlString(String text) {
if (EmptyUtils.isNullOrEmpty(text)) {
return text;
}
StringBuilder sb = new StringBuilder();
char ch;
for (int i = 0; i < text.length(); i++) {
ch = text.charAt(i);
if(((ch >= 0x20) && (ch <= 0xD7FF)) ||
(ch == 0x9) || (ch == 0xA) || (ch == 0xD) ||
((ch >= 0xE000) && (ch <= 0xFFFD))) {
sb.append(ch);
}
}
return sb.toString();
}
}