diff --git a/LICENSE b/LICENSE index ce4c89cf7..0dbc5557d 100644 --- a/LICENSE +++ b/LICENSE @@ -119,3 +119,18 @@ Files under jni/openssl-0.98l/*: * */ +Files under biz.source_code.base64Coder +// Copyright 2003-2010 Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland +// www.source-code.biz, www.inventec.ch/chdh +// +// This module is multi-licensed and may be used under the terms +// of any of the following licenses: +// +// EPL, Eclipse Public License, http://www.eclipse.org/legal +// LGPL, GNU Lesser General Public License, http://www.gnu.org/licenses/lgpl.html +// AL, Apache License, http://www.apache.org/licenses +// BSD, BSD License, http://www.opensource.org/licenses/bsd-license.php +// +// Please contact the author if you need another license. +// This module is provided "as is", without warranties of any kind. + diff --git a/src/biz/source_code/base64Coder/Base64Coder.java b/src/biz/source_code/base64Coder/Base64Coder.java new file mode 100644 index 000000000..9054587d5 --- /dev/null +++ b/src/biz/source_code/base64Coder/Base64Coder.java @@ -0,0 +1,225 @@ +// Copyright 2003-2010 Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland +// www.source-code.biz, www.inventec.ch/chdh +// +// This module is multi-licensed and may be used under the terms +// of any of the following licenses: +// +// EPL, Eclipse Public License, http://www.eclipse.org/legal +// LGPL, GNU Lesser General Public License, http://www.gnu.org/licenses/lgpl.html +// AL, Apache License, http://www.apache.org/licenses +// BSD, BSD License, http://www.opensource.org/licenses/bsd-license.php +// +// Please contact the author if you need another license. +// This module is provided "as is", without warranties of any kind. + +package biz.source_code.base64Coder; + +/** +* A Base64 encoder/decoder. +* +*

+* This class is used to encode and decode data in Base64 format as described in RFC 1521. +* +*

+* Project home page: www.source-code.biz/base64coder/java
+* Author: Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland
+* Multi-licensed: EPL / LGPL / AL / BSD. +*/ +public class Base64Coder { + +// The line separator string of the operating system. +private static final String systemLineSeparator = System.getProperty("line.separator"); + +// Mapping table from 6-bit nibbles to Base64 characters. +private static char[] map1 = new char[64]; + static { + int i=0; + for (char c='A'; c<='Z'; c++) map1[i++] = c; + for (char c='a'; c<='z'; c++) map1[i++] = c; + for (char c='0'; c<='9'; c++) map1[i++] = c; + map1[i++] = '+'; map1[i++] = '/'; } + +// Mapping table from Base64 characters to 6-bit nibbles. +private static byte[] map2 = new byte[128]; + static { + for (int i=0; isun.misc.BASE64Encoder.encodeBuffer(byte[]). +* @param in An array containing the data bytes to be encoded. +* @return A String containing the Base64 encoded data, broken into lines. +*/ +public static String encodeLines (byte[] in) { + return encodeLines(in, 0, in.length, 76, systemLineSeparator); } + +/** +* Encodes a byte array into Base 64 format and breaks the output into lines. +* @param in An array containing the data bytes to be encoded. +* @param iOff Offset of the first byte in in to be processed. +* @param iLen Number of bytes to be processed in in, starting at iOff. +* @param lineLen Line length for the output data. Should be a multiple of 4. +* @param lineSeparator The line separator to be used to separate the output lines. +* @return A String containing the Base64 encoded data, broken into lines. +*/ +public static String encodeLines (byte[] in, int iOff, int iLen, int lineLen, String lineSeparator) { + int blockLen = (lineLen*3) / 4; + if (blockLen <= 0) throw new IllegalArgumentException(); + int lines = (iLen+blockLen-1) / blockLen; + int bufLen = ((iLen+2)/3)*4 + lines*lineSeparator.length(); + StringBuilder buf = new StringBuilder(bufLen); + int ip = 0; + while (ip < iLen) { + int l = Math.min(iLen-ip, blockLen); + buf.append (encode(in, iOff+ip, l)); + buf.append (lineSeparator); + ip += l; } + return buf.toString(); } + +/** +* Encodes a byte array into Base64 format. +* No blanks or line breaks are inserted in the output. +* @param in An array containing the data bytes to be encoded. +* @return A character array containing the Base64 encoded data. +*/ +public static char[] encode (byte[] in) { + return encode(in, 0, in.length); } + +/** +* Encodes a byte array into Base64 format. +* No blanks or line breaks are inserted in the output. +* @param in An array containing the data bytes to be encoded. +* @param iLen Number of bytes to process in in. +* @return A character array containing the Base64 encoded data. +*/ +public static char[] encode (byte[] in, int iLen) { + return encode(in, 0, iLen); } + +/** +* Encodes a byte array into Base64 format. +* No blanks or line breaks are inserted in the output. +* @param in An array containing the data bytes to be encoded. +* @param iOff Offset of the first byte in in to be processed. +* @param iLen Number of bytes to process in in, starting at iOff. +* @return A character array containing the Base64 encoded data. +*/ +public static char[] encode (byte[] in, int iOff, int iLen) { + int oDataLen = (iLen*4+2)/3; // output length without padding + int oLen = ((iLen+2)/3)*4; // output length including padding + char[] out = new char[oLen]; + int ip = iOff; + int iEnd = iOff + iLen; + int op = 0; + while (ip < iEnd) { + int i0 = in[ip++] & 0xff; + int i1 = ip < iEnd ? in[ip++] & 0xff : 0; + int i2 = ip < iEnd ? in[ip++] & 0xff : 0; + int o0 = i0 >>> 2; + int o1 = ((i0 & 3) << 4) | (i1 >>> 4); + int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6); + int o3 = i2 & 0x3F; + out[op++] = map1[o0]; + out[op++] = map1[o1]; + out[op] = op < oDataLen ? map1[o2] : '='; op++; + out[op] = op < oDataLen ? map1[o3] : '='; op++; } + return out; } + +/** +* Decodes a string from Base64 format. +* No blanks or line breaks are allowed within the Base64 encoded input data. +* @param s A Base64 String to be decoded. +* @return A String containing the decoded data. +* @throws IllegalArgumentException If the input is not valid Base64 encoded data. +*/ +public static String decodeString (String s) { + return new String(decode(s)); } + +/** +* Decodes a byte array from Base64 format and ignores line separators, tabs and blanks. +* CR, LF, Tab and Space characters are ignored in the input data. +* This method is compatible with sun.misc.BASE64Decoder.decodeBuffer(String). +* @param s A Base64 String to be decoded. +* @return An array containing the decoded data bytes. +* @throws IllegalArgumentException If the input is not valid Base64 encoded data. +*/ +public static byte[] decodeLines (String s) { + char[] buf = new char[s.length()]; + int p = 0; + for (int ip = 0; ip < s.length(); ip++) { + char c = s.charAt(ip); + if (c != ' ' && c != '\r' && c != '\n' && c != '\t') + buf[p++] = c; } + return decode(buf, 0, p); } + +/** +* Decodes a byte array from Base64 format. +* No blanks or line breaks are allowed within the Base64 encoded input data. +* @param s A Base64 String to be decoded. +* @return An array containing the decoded data bytes. +* @throws IllegalArgumentException If the input is not valid Base64 encoded data. +*/ +public static byte[] decode (String s) { + return decode(s.toCharArray()); } + +/** +* Decodes a byte array from Base64 format. +* No blanks or line breaks are allowed within the Base64 encoded input data. +* @param in A character array containing the Base64 encoded data. +* @return An array containing the decoded data bytes. +* @throws IllegalArgumentException If the input is not valid Base64 encoded data. +*/ +public static byte[] decode (char[] in) { + return decode(in, 0, in.length); } + +/** +* Decodes a byte array from Base64 format. +* No blanks or line breaks are allowed within the Base64 encoded input data. +* @param in A character array containing the Base64 encoded data. +* @param iOff Offset of the first character in in to be processed. +* @param iLen Number of characters to process in in, starting at iOff. +* @return An array containing the decoded data bytes. +* @throws IllegalArgumentException If the input is not valid Base64 encoded data. +*/ +public static byte[] decode (char[] in, int iOff, int iLen) { + if (iLen%4 != 0) throw new IllegalArgumentException ("Length of Base64 encoded input string is not a multiple of 4."); + while (iLen > 0 && in[iOff+iLen-1] == '=') iLen--; + int oLen = (iLen*3) / 4; + byte[] out = new byte[oLen]; + int ip = iOff; + int iEnd = iOff + iLen; + int op = 0; + while (ip < iEnd) { + int i0 = in[ip++]; + int i1 = in[ip++]; + int i2 = ip < iEnd ? in[ip++] : 'A'; + int i3 = ip < iEnd ? in[ip++] : 'A'; + if (i0 > 127 || i1 > 127 || i2 > 127 || i3 > 127) + throw new IllegalArgumentException ("Illegal character in Base64 encoded data."); + int b0 = map2[i0]; + int b1 = map2[i1]; + int b2 = map2[i2]; + int b3 = map2[i3]; + if (b0 < 0 || b1 < 0 || b2 < 0 || b3 < 0) + throw new IllegalArgumentException ("Illegal character in Base64 encoded data."); + int o0 = ( b0 <<2) | (b1>>>4); + int o1 = ((b1 & 0xf)<<4) | (b2>>>2); + int o2 = ((b2 & 3)<<6) | b3; + out[op++] = (byte)o0; + if (op(cur)); + entries.put(cur.getUUID(), new WeakReference(cur)); } for (int i = 0; i < childGroups.size(); i++ ) { diff --git a/src/com/keepassdroid/EntryActivity.java b/src/com/keepassdroid/EntryActivity.java index 14c09a448..58fe5171c 100644 --- a/src/com/keepassdroid/EntryActivity.java +++ b/src/com/keepassdroid/EntryActivity.java @@ -77,7 +77,7 @@ public class EntryActivity extends LockCloseActivity { public static void Launch(Activity act, PwEntry pw, int pos) { Intent i = new Intent(act, EntryActivity.class); - i.putExtra(KEY_ENTRY, pw.uuid); + i.putExtra(KEY_ENTRY, Types.UUIDtoBytes(pw.getUUID())); i.putExtra(KEY_REFRESH_POS, pos); act.startActivityForResult(i,0); diff --git a/src/com/keepassdroid/EntryEditActivity.java b/src/com/keepassdroid/EntryEditActivity.java index 8c27de2b4..d3f053fec 100644 --- a/src/com/keepassdroid/EntryEditActivity.java +++ b/src/com/keepassdroid/EntryEditActivity.java @@ -70,7 +70,7 @@ public class EntryEditActivity extends LockCloseActivity { Intent i = new Intent(act, EntryEditActivity.class); - i.putExtra(KEY_ENTRY, pw.uuid); + i.putExtra(KEY_ENTRY, Types.UUIDtoBytes(pw.getUUID())); act.startActivityForResult(i, 0); } @@ -155,7 +155,7 @@ public class EntryEditActivity extends LockCloseActivity { newEntry.parent = mEntry.parent; newEntry.tCreation = mEntry.tCreation; newEntry.tExpire = mEntry.tExpire; - newEntry.uuid = mEntry.uuid; + newEntry.setUUID(mEntry.getUUID()); Date now = Calendar.getInstance().getTime(); newEntry.tLastAccess = new PwDate(now); diff --git a/src/com/keepassdroid/crypto/PwStreamCipherFactory.java b/src/com/keepassdroid/crypto/PwStreamCipherFactory.java new file mode 100644 index 000000000..1284f28da --- /dev/null +++ b/src/com/keepassdroid/crypto/PwStreamCipherFactory.java @@ -0,0 +1,65 @@ +/* + * Copyright 2009 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 . + * + */ +package com.keepassdroid.crypto; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.crypto.StreamCipher; +import org.bouncycastle.crypto.engines.Salsa20Engine; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; + +import com.keepassdroid.database.CrsAlgorithm; + +public class PwStreamCipherFactory { + public static StreamCipher getInstance(CrsAlgorithm alg, byte[] key) { + if ( alg == CrsAlgorithm.Salsa20 ) { + return getSalsa20(key); + + } else { + return null; + } + } + + + private static final byte[] SALSA_IV = new byte[]{ (byte)0xE8, 0x30, 0x09, 0x4B, + (byte)0x97, 0x20, 0x5D, 0x2A }; + + private static StreamCipher getSalsa20(byte[] key) { + // Build stream cipher key + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + throw new RuntimeException("SHA 256 not supported"); + } + byte[] key32 = md.digest(key); + + KeyParameter keyParam = new KeyParameter(key32); + ParametersWithIV ivParam = new ParametersWithIV(keyParam, SALSA_IV); + + StreamCipher cipher = new Salsa20Engine(); + cipher.init(true, ivParam); + + return cipher; + } +} diff --git a/src/com/keepassdroid/database/CrsAlgorithm.java b/src/com/keepassdroid/database/CrsAlgorithm.java index caa4ef4a0..f6878473f 100644 --- a/src/com/keepassdroid/database/CrsAlgorithm.java +++ b/src/com/keepassdroid/database/CrsAlgorithm.java @@ -19,10 +19,27 @@ */ package com.keepassdroid.database; -public class CrsAlgorithm { - public static final int Null = 0; - public static final int ArcFourVariant = 1; - public static final int Salsa20 = 2; +public enum CrsAlgorithm { - public static final int Count = 3; + Null(0), + ArcFourVariant(1), + Salsa20(2); + + public static final int count = 3; + private final int id; + + private CrsAlgorithm(int num) { + id = num; + } + + public static CrsAlgorithm fromId(int num) { + for ( CrsAlgorithm e : CrsAlgorithm.values() ) { + if ( e.id == num ) { + return e; + } + } + + return null; + } + } diff --git a/src/com/keepassdroid/database/ITimeLogger.java b/src/com/keepassdroid/database/ITimeLogger.java new file mode 100644 index 000000000..2b62c8611 --- /dev/null +++ b/src/com/keepassdroid/database/ITimeLogger.java @@ -0,0 +1,46 @@ +/* + * Copyright 2010 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 . + * + */ +package com.keepassdroid.database; + +import java.util.Date; + +public interface ITimeLogger { + Date getLastModificationTime(); + void setLastModificationTime(Date date); + + Date getCreationTime(); + void setCreationTime(Date date); + + Date getLastAccessTime(); + void setLastAccessTime(Date date); + + Date getExpiryTime(); + void setExpiryTime(Date date); + + boolean expires(); + void setExpires(boolean exp); + + long getUsageCount(); + void setUsageCount(long count); + + Date getLocationChanged(); + void setLocationChanged(Date date); + +} diff --git a/src/com/keepassdroid/database/PwCustomIcon.java b/src/com/keepassdroid/database/PwCustomIcon.java new file mode 100644 index 000000000..ec38994a6 --- /dev/null +++ b/src/com/keepassdroid/database/PwCustomIcon.java @@ -0,0 +1,32 @@ +/* + * Copyright 2010 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 . + * + */ +package com.keepassdroid.database; + +import java.util.UUID; + +public class PwCustomIcon { + private UUID uuid; + private byte[] imageData; + + public PwCustomIcon(UUID u, byte[] data) { + uuid = u; + imageData = data; + } +} diff --git a/src/com/keepassdroid/database/PwDatabase.java b/src/com/keepassdroid/database/PwDatabase.java index 755321ea2..57386e0db 100644 --- a/src/com/keepassdroid/database/PwDatabase.java +++ b/src/com/keepassdroid/database/PwDatabase.java @@ -38,6 +38,7 @@ public abstract class PwDatabase { public byte masterKey[] = new byte[32]; public byte[] finalKey; + public String name = "KeePass database"; public void makeFinalKey(byte[] masterSeed, byte[] masterSeed2, int numRounds) throws IOException { diff --git a/src/com/keepassdroid/database/PwDatabaseV3.java b/src/com/keepassdroid/database/PwDatabaseV3.java index 62a0000a2..c144e5d63 100644 --- a/src/com/keepassdroid/database/PwDatabaseV3.java +++ b/src/com/keepassdroid/database/PwDatabaseV3.java @@ -48,9 +48,6 @@ public class PwDatabaseV3 extends PwDatabase { // Constants // private static final int PWM_SESSION_KEY_SIZE = 12; - // Descriptive name for database, used in GUI. - public String name = "KeePass database"; - // Special entry for settings public PwEntry metaInfo; @@ -153,7 +150,7 @@ public class PwDatabaseV3 extends PwDatabase { groups.addElement(group); } - public void addEntry(PwEntryV3 entry) + public void addEntry(PwEntry entry) { entries.addElement(entry); } diff --git a/src/com/keepassdroid/database/PwDatabaseV4.java b/src/com/keepassdroid/database/PwDatabaseV4.java index 4aec808bb..d4f10683b 100644 --- a/src/com/keepassdroid/database/PwDatabaseV4.java +++ b/src/com/keepassdroid/database/PwDatabaseV4.java @@ -20,15 +20,17 @@ package com.keepassdroid.database; import java.io.IOException; +import java.io.InputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.Vector; -import org.w3c.dom.Document; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - import com.keepassdroid.database.exception.InconsistentDBException; import com.keepassdroid.database.exception.InvalidKeyFileException; @@ -38,10 +40,48 @@ public class PwDatabaseV4 extends PwDatabase { public UUID dataCipher; public PwCompressionAlgorithm compressionAlgorithm; public long numKeyEncRounds; - private Document doc; + public Date nameChanged; + public String description; + public Date descriptionChanged; + public String defaultUserName; + public Date defaultUserNameChanged; + public long maintenanceHistoryDays; + public boolean recycleBinEnabled; + public UUID recycleBinUUID; + public Date recycleBinChanged; + public UUID entryTemplatesGroup; + public Date entryTemplatesGroupChanged; + public UUID lastSelectedGroup; + public UUID lastTopVisibleGroup; + public MemoryProtectionConfig memoryProtection = new MemoryProtectionConfig(); + public List deletedObjects = new ArrayList(); + public List customIcons; + public Map customData = new HashMap(); + + 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; + } + } + + public static final UUID UUID_ZERO = new UUID(0,0); //private Vector groups = new Vector(); - private PwGroupV4 rootGroup; + public PwGroupV4 rootGroup; @Override public byte[] getMasterKey(String key, String keyFileName) @@ -83,19 +123,8 @@ public class PwDatabaseV4 extends PwDatabase { return list; } - public void parseDB(Document d) throws InconsistentDBException { - doc = d; - - NodeList list = doc.getElementsByTagName("Root"); - - int len = list.getLength(); - if ( len < 0 || len > 1 ) { - throw new InconsistentDBException("Missing root node"); - } - - Node root = list.item(0); - - rootGroup = new PwGroupV4(root); + public void parseDB(InputStream in) throws InconsistentDBException { + //TODO Implement Me } @Override diff --git a/src/com/keepassdroid/database/PwDbHeaderV4.java b/src/com/keepassdroid/database/PwDbHeaderV4.java index 230d8f1cf..94788174c 100644 --- a/src/com/keepassdroid/database/PwDbHeaderV4.java +++ b/src/com/keepassdroid/database/PwDbHeaderV4.java @@ -52,7 +52,7 @@ public class PwDbHeaderV4 extends PwDbHeader { private PwDatabaseV4 db; public byte[] protectedStreamKey; public byte[] streamStartBytes; - public int innerRandomStream; + public CrsAlgorithm innerRandomStream; public PwDbHeaderV4(PwDatabaseV4 d) { db = d; @@ -190,11 +190,11 @@ public class PwDbHeaderV4 extends PwDbHeader { } int id = LEDataInputStream.readInt(streamID, 0); - if ( id < 0 || id >= CrsAlgorithm.Count ) { + if ( id < 0 || id >= CrsAlgorithm.count ) { throw new IOException("Invalid stream id."); } - innerRandomStream = id; + innerRandomStream = CrsAlgorithm.fromId(id); } /** Determines if this is a supported version. diff --git a/src/com/keepassdroid/database/PwDefsV4.java b/src/com/keepassdroid/database/PwDefsV4.java new file mode 100644 index 000000000..62907c44d --- /dev/null +++ b/src/com/keepassdroid/database/PwDefsV4.java @@ -0,0 +1,34 @@ +/* + * Copyright 2010 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 . + * + */ +package com.keepassdroid.database; + +public class PwDefsV4 { + + public static final String TITLE_FIELD = "Title"; + + public static final String USERNAME_FIELD = "UserName"; + + public static final String PASSWORD_FIELD = "Password"; + + public static final String URL_FIELD = "URL"; + + public static final String NOTES_FIELD = "Notes"; + +} diff --git a/src/com/keepassdroid/database/PwDeletedObject.java b/src/com/keepassdroid/database/PwDeletedObject.java new file mode 100644 index 000000000..ea94b9322 --- /dev/null +++ b/src/com/keepassdroid/database/PwDeletedObject.java @@ -0,0 +1,40 @@ +/* + * Copyright 2010 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 . + * + */ +package com.keepassdroid.database; + +import java.util.Date; +import java.util.UUID; + +public class PwDeletedObject { + public UUID uuid; + private Date deletionTime; + + public Date getDeletionTime() { + if ( deletionTime == null ) { + return new Date(System.currentTimeMillis()); + } + + return deletionTime; + } + + public void setDeletionTime(Date date) { + deletionTime = date; + } +} diff --git a/src/com/keepassdroid/database/PwEntry.java b/src/com/keepassdroid/database/PwEntry.java index b9898e41b..872c18f9e 100644 --- a/src/com/keepassdroid/database/PwEntry.java +++ b/src/com/keepassdroid/database/PwEntry.java @@ -20,13 +20,15 @@ package com.keepassdroid.database; import java.util.Date; +import java.util.UUID; public abstract class PwEntry implements Cloneable { - public byte uuid[] = new byte[16]; + //public byte uuid[] = new byte[16]; public String title; public String url; public String additional; + public int imageId; public PwEntry() { @@ -42,7 +44,7 @@ public abstract class PwEntry implements Cloneable { throw new RuntimeException("Clone should be supported"); } - System.arraycopy(uuid, 0, newEntry.uuid, 0, uuid.length); + newEntry.setUUID(getUUID()); newEntry.title = title; newEntry.url = url; newEntry.additional = additional; @@ -51,14 +53,16 @@ public abstract class PwEntry implements Cloneable { } public void assign(PwEntry source) { - System.arraycopy(source.uuid, 0, uuid, 0, source.uuid.length); + setUUID(source.getUUID()); title = source.title; url = source.url; additional = source.additional; } public abstract void stampLastAccess(); - + + public abstract UUID getUUID(); + public abstract void setUUID(UUID u); public abstract String getUsername(); public abstract String getPassword(); public abstract Date getCreate(); diff --git a/src/com/keepassdroid/database/PwEntryV3.java b/src/com/keepassdroid/database/PwEntryV3.java index 123cad74d..265ea4065 100644 --- a/src/com/keepassdroid/database/PwEntryV3.java +++ b/src/com/keepassdroid/database/PwEntryV3.java @@ -29,11 +29,13 @@ import java.lang.ref.WeakReference; import java.util.Calendar; import java.util.Date; import java.util.Random; +import java.util.UUID; import android.util.Log; import com.keepassdroid.Database; +import com.keepassdroid.utils.Types; /** @@ -71,11 +73,9 @@ public class PwEntryV3 extends PwEntry { public int groupId; - public int imageId; - - public String username; - + public String username; private byte[] password; + private byte[] uuid; public PwDate tCreation; public PwDate tLastMod; @@ -315,11 +315,6 @@ public class PwEntryV3 extends PwEntry { } - @Override - public String getUsername() { - return username; - } - @Override public Date getAccess() { return tLastAccess.getJDate(); @@ -345,4 +340,19 @@ public class PwEntryV3 extends PwEntry { return parent; } + @Override + public UUID getUUID() { + return Types.bytestoUUID(uuid); + } + + @Override + public void setUUID(UUID u) { + uuid = Types.UUIDtoBytes(u); + } + + @Override + public String getUsername() { + return username; + } + } diff --git a/src/com/keepassdroid/database/PwEntryV4.java b/src/com/keepassdroid/database/PwEntryV4.java index e11e0173b..3df2b4447 100644 --- a/src/com/keepassdroid/database/PwEntryV4.java +++ b/src/com/keepassdroid/database/PwEntryV4.java @@ -19,15 +19,50 @@ */ package com.keepassdroid.database; +import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; -import org.w3c.dom.Node; - -public class PwEntryV4 extends PwEntry { - private Node node; +public class PwEntryV4 extends PwEntry implements ITimeLogger { + private static final String STR_USERNAME = "UserName"; - public PwEntryV4(Node n) { - node = n; + public PwGroupV4 parent; + public UUID uuid; + public Map strings = new HashMap(); + public Map binaries = new HashMap(); + public UUID customIconUuid; + public String foregroundColor; + public String backgroupColor; + public String overrideURL; + public AutoType autoType = new AutoType(); + public List history = new ArrayList(); + + private Date parentGroupLastMod; + private Date creation; + private Date lastMod; + private Date lastAccess; + private Date expireDate; + private boolean expires = false; + private long usageCount = 0; + + + public class AutoType { + public boolean enabled; + public long obfuscationOptions; + public String defaultSequence; + + private Map windowSeqPairs = new HashMap(); + + public void put(String key, String value) { + windowSeqPairs.put(key, value); + } + } + + public PwEntryV4() { + } @Override @@ -43,7 +78,7 @@ public class PwEntryV4 extends PwEntry { } private void assign(PwEntryV4 source) { - node = source.node; + // TODO: Implement me } @Override @@ -60,50 +95,131 @@ public class PwEntryV4 extends PwEntry { @Override public String getUsername() { - // TODO Implement me - return null; + return getString(STR_USERNAME); } @Override public String getPassword() { - // TODO Implement me return null; } @Override public Date getAccess() { - // TODO Auto-generated method stub return null; } @Override public Date getCreate() { - // TODO Auto-generated method stub return null; } @Override public Date getExpire() { - // TODO Auto-generated method stub return null; } @Override public Date getMod() { - // TODO Auto-generated method stub - return null; + return parentGroupLastMod; } @Override public String getDisplayTitle() { - // TOOD: Add special TAN handling for V4? return title; } @Override public PwGroupV4 getParent() { - // TODO Auto-generated method stub - return null; + return parent; } + @Override + public UUID getUUID() { + return uuid; + } + + + @Override + public void setUUID(UUID u) { + uuid = u; + } + + public String getString(String key) { + String value = strings.get(key); + + if ( value == null ) return new String(""); + + return value; + } + + @Override + public Date getCreationTime() { + return creation; + } + + @Override + public Date getExpiryTime() { + return expireDate; + } + + @Override + public Date getLastAccessTime() { + return lastAccess; + } + + @Override + public Date getLastModificationTime() { + return lastMod; + } + + @Override + public Date getLocationChanged() { + return parentGroupLastMod; + } + + @Override + public long getUsageCount() { + return usageCount; + } + + @Override + public void setCreationTime(Date date) { + creation = date; + + } + + @Override + public void setExpiryTime(Date date) { + expireDate = date; + } + + @Override + public void setLastAccessTime(Date date) { + lastAccess = date; + } + + @Override + public void setLastModificationTime(Date date) { + lastMod = date; + } + + @Override + public void setLocationChanged(Date date) { + parentGroupLastMod = date; + } + + @Override + public void setUsageCount(long count) { + usageCount = count; + } + + @Override + public boolean expires() { + return expires; + } + + @Override + public void setExpires(boolean exp) { + expires = exp; + } } diff --git a/src/com/keepassdroid/database/PwGroup.java b/src/com/keepassdroid/database/PwGroup.java index 5fe734c5f..bc53c801d 100644 --- a/src/com/keepassdroid/database/PwGroup.java +++ b/src/com/keepassdroid/database/PwGroup.java @@ -19,6 +19,7 @@ */ package com.keepassdroid.database; +import java.util.Date; import java.util.Vector; public abstract class PwGroup { @@ -30,5 +31,7 @@ public abstract class PwGroup { public abstract PwGroupId getId(); public abstract String getName(); + + public abstract Date getLastMod(); } diff --git a/src/com/keepassdroid/database/PwGroupV3.java b/src/com/keepassdroid/database/PwGroupV3.java index 4356af204..fc5cf2883 100644 --- a/src/com/keepassdroid/database/PwGroupV3.java +++ b/src/com/keepassdroid/database/PwGroupV3.java @@ -118,4 +118,9 @@ public class PwGroupV3 extends PwGroup { return name; } + @Override + public Date getLastMod() { + return tLastMod.getJDate(); + } + } diff --git a/src/com/keepassdroid/database/PwGroupV4.java b/src/com/keepassdroid/database/PwGroupV4.java index 009a55bbb..b6894bb43 100644 --- a/src/com/keepassdroid/database/PwGroupV4.java +++ b/src/com/keepassdroid/database/PwGroupV4.java @@ -19,45 +19,70 @@ */ package com.keepassdroid.database; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; import java.util.UUID; import java.util.Vector; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; +public class PwGroupV4 extends PwGroup implements ITimeLogger { -import com.keepassdroid.database.exception.InconsistentDBException; - -public class PwGroupV4 extends PwGroup { - - private Node node; - private PwGroup parent; - - public PwGroupV4(Node n) throws InconsistentDBException { - this(n, null); - } - - public PwGroupV4(Node n, PwGroup p) throws InconsistentDBException { - node = n; - parent = p; - buildTree(); - } - - private void buildTree() throws InconsistentDBException { - NodeList children = node.getChildNodes(); + public PwGroupV4 parent = null; + public UUID uuid; + public String name; + public String notes; + public int iconId; + public UUID customIconUuid; + public boolean isExpanded; + public String defaultAutoTypeSequence; + public Boolean enableAutoType; + public Boolean enableSearching; + public UUID lastTopVisibleEntry; + private Date parentGroupLastMod; + private Date creation; + private Date lastMod; + private Date lastAccess; + private Date expireDate; + private boolean expires = false; + private long usageCount = 0; + + + public List listGroups = new ArrayList(); + public List listEntries = new ArrayList(); + + public PwGroupV4() { - for ( int i = 0; i < children.getLength(); i++ ) { - Node child = children.item(i); - String name = child.getNodeName(); - if ( name.equalsIgnoreCase("Group") ) { - PwGroupV4 group = new PwGroupV4(child, this); - childGroups.add(group); - } else if ( name.equalsIgnoreCase("Entry") ) { - PwEntryV4 entry = new PwEntryV4(child); - childEntries.add(entry); - } - } } - + + public void AddGroup(PwGroupV4 subGroup, boolean takeOwnership) { + AddGroup(subGroup, takeOwnership, false); + } + + public void AddGroup(PwGroupV4 subGroup, boolean takeOwnership, boolean updateLocationChanged) { + if ( subGroup == null ) throw new RuntimeException("subGroup"); + + listGroups.add(subGroup); + + if ( takeOwnership ) subGroup.parent = this; + + if ( updateLocationChanged ) subGroup.parentGroupLastMod = new Date(System.currentTimeMillis()); + + } + + public void AddEntry(PwEntryV4 pe, boolean takeOwnership) { + AddEntry(pe, takeOwnership, false); + } + + public void AddEntry(PwEntryV4 pe, boolean takeOwnership, boolean updateLocationChanged) { + assert(pe != null); + + listEntries.add(pe); + + if ( takeOwnership ) pe.parent = this; + + if ( updateLocationChanged ) pe.setLocationChanged(new Date(System.currentTimeMillis())); + } + @Override public PwGroup getParent() { return parent; @@ -87,18 +112,88 @@ public class PwGroupV4 extends PwGroup { @Override public PwGroupId getId() { - return new PwGroupIdV4(getUUID()); - } - - public UUID getUUID() { - // TODO: Get UUID from document - return null; + return new PwGroupIdV4(uuid); } @Override public String getName() { - // TODO Auto-generated method stub - return null; + return name; + } + + @Override + public Date getLastMod() { + return parentGroupLastMod; + } + + @Override + public Date getCreationTime() { + return creation; + } + + @Override + public Date getExpiryTime() { + return expireDate; + } + + @Override + public Date getLastAccessTime() { + return lastAccess; + } + + @Override + public Date getLastModificationTime() { + return lastMod; + } + + @Override + public Date getLocationChanged() { + return parentGroupLastMod; + } + + @Override + public long getUsageCount() { + return usageCount; + } + + @Override + public void setCreationTime(Date date) { + creation = date; + + } + + @Override + public void setExpiryTime(Date date) { + expireDate = date; + } + + @Override + public void setLastAccessTime(Date date) { + lastAccess = date; + } + + @Override + public void setLastModificationTime(Date date) { + lastMod = date; + } + + @Override + public void setLocationChanged(Date date) { + parentGroupLastMod = date; + } + + @Override + public void setUsageCount(long count) { + usageCount = count; + } + + @Override + public boolean expires() { + return expires; + } + + @Override + public void setExpires(boolean exp) { + expires = exp; } diff --git a/src/com/keepassdroid/database/edit/AddEntry.java b/src/com/keepassdroid/database/edit/AddEntry.java index e2e1f4572..6665b678f 100644 --- a/src/com/keepassdroid/database/edit/AddEntry.java +++ b/src/com/keepassdroid/database/edit/AddEntry.java @@ -34,7 +34,7 @@ public abstract class AddEntry extends RunnableOnFinish { public static AddEntry getInstance(Database db, PwEntry entry, OnFinish finish) { if ( entry instanceof PwEntryV3 ) { - return new AddEntryV3(db, (PwEntryV3) entry, finish); + return new AddEntryV3(db, (PwEntry) entry, finish); } else { // TODO: Implement me throw new RuntimeException("Not implemented yet."); @@ -76,7 +76,7 @@ public abstract class AddEntry extends RunnableOnFinish { mDb.dirty.put(parent, new WeakReference(parent)); // Add entry to global - mDb.entries.put(Types.bytestoUUID(mEntry.uuid), new WeakReference(mEntry)); + mDb.entries.put(mEntry.getUUID(), new WeakReference(mEntry)); if ( mDb.indexBuilt ) { // Add entry to search index diff --git a/src/com/keepassdroid/database/edit/AddEntryV3.java b/src/com/keepassdroid/database/edit/AddEntryV3.java index fc7df94f4..8454c2e38 100644 --- a/src/com/keepassdroid/database/edit/AddEntryV3.java +++ b/src/com/keepassdroid/database/edit/AddEntryV3.java @@ -20,15 +20,15 @@ package com.keepassdroid.database.edit; import com.keepassdroid.Database; -import com.keepassdroid.database.PwDatabaseV3; -import com.keepassdroid.database.PwEntryV3; +import com.keepassdroid.database.PwDatabase; +import com.keepassdroid.database.PwEntry; import com.keepassdroid.database.PwGroupV3; public class AddEntryV3 extends AddEntry { - private PwEntryV3 mEntry; + private PwEntry mEntry; - protected AddEntryV3(Database db, PwEntryV3 entry, OnFinish finish) { + protected AddEntryV3(Database db, PwEntry entry, OnFinish finish) { super(db, entry, finish); mEntry = entry; @@ -36,13 +36,13 @@ public class AddEntryV3 extends AddEntry { public void addEntry() { - PwGroupV3 parent = mEntry.getParent(); + PwGroupV3 parent = (PwGroupV3) mEntry.getParent(); // Add entry to group parent.childEntries.add(mEntry); // Add entry to PwDatabaseV3 - PwDatabaseV3 pm = (PwDatabaseV3) mDb.pm; + PwDatabase pm = (PwDatabase) mDb.pm; pm.getEntries().add(mEntry); // Sort entries diff --git a/src/com/keepassdroid/database/load/ImporterV3.java b/src/com/keepassdroid/database/load/ImporterV3.java index 05f2006de..b150c2990 100644 --- a/src/com/keepassdroid/database/load/ImporterV3.java +++ b/src/com/keepassdroid/database/load/ImporterV3.java @@ -107,7 +107,7 @@ public class ImporterV3 extends Importer { return openDatabase(inStream, password, keyfile, new UpdateStatus()); } - public PwDatabaseV3 openDatabase( InputStream inStream, String password, String keyfile, UpdateStatus status ) + public PwDatabase openDatabase( InputStream inStream, String password, String keyfile, UpdateStatus status ) throws IOException, InvalidKeyFileException, InvalidPasswordException, InvalidDBSignatureException, InvalidDBVersionException { PwDatabaseV3 newManager; @@ -375,7 +375,7 @@ public class ImporterV3 extends Importer { // Ignore field break; case 0x0001 : - System.arraycopy(buf, offset, ent.uuid, 0, 16); + ent.setUUID(Types.bytestoUUID(buf, offset)); break; case 0x0002 : ent.groupId = LEDataInputStream.readInt(buf, offset); diff --git a/src/com/keepassdroid/database/load/ImporterV4.java b/src/com/keepassdroid/database/load/ImporterV4.java index 11f3cdc21..79f919275 100644 --- a/src/com/keepassdroid/database/load/ImporterV4.java +++ b/src/com/keepassdroid/database/load/ImporterV4.java @@ -19,30 +19,41 @@ */ package com.keepassdroid.database.load; -import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.Arrays; +import java.util.Date; +import java.util.Stack; +import java.util.UUID; import java.util.zip.GZIPInputStream; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import org.w3c.dom.Document; -import org.xml.sax.SAXException; +import org.bouncycastle.crypto.StreamCipher; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import biz.source_code.base64Coder.Base64Coder; import com.keepassdroid.UpdateStatus; import com.keepassdroid.crypto.CipherFactory; +import com.keepassdroid.crypto.PwStreamCipherFactory; +import com.keepassdroid.database.ITimeLogger; import com.keepassdroid.database.PwCompressionAlgorithm; +import com.keepassdroid.database.PwCustomIcon; import com.keepassdroid.database.PwDatabaseV4; import com.keepassdroid.database.PwDbHeaderV4; -import com.keepassdroid.database.exception.InconsistentDBException; +import com.keepassdroid.database.PwDeletedObject; +import com.keepassdroid.database.PwEntryV4; +import com.keepassdroid.database.PwGroupV4; import com.keepassdroid.database.exception.InvalidDBSignatureException; import com.keepassdroid.database.exception.InvalidDBVersionException; import com.keepassdroid.database.exception.InvalidKeyFileException; @@ -50,8 +61,12 @@ import com.keepassdroid.database.exception.InvalidPasswordException; import com.keepassdroid.stream.BetterCipherInputStream; import com.keepassdroid.stream.HashedBlockInputStream; import com.keepassdroid.stream.LEDataInputStream; +import com.keepassdroid.utils.Types; public class ImporterV4 extends Importer { + + private StreamCipher randomStream; + private PwDatabaseV4 db; @Override public PwDatabaseV4 openDatabase(InputStream inStream, String password, @@ -67,7 +82,7 @@ public class ImporterV4 extends Importer { InvalidKeyFileException, InvalidPasswordException, InvalidDBSignatureException, InvalidDBVersionException { - PwDatabaseV4 db = new PwDatabaseV4(); + db = new PwDatabaseV4(); PwDbHeaderV4 header = new PwDbHeaderV4(db); @@ -112,6 +127,26 @@ public class ImporterV4 extends Importer { decompressed = hashed; } + if ( header.protectedStreamKey == null ) { + assert(false); + throw new IOException("Invalid stream key."); + } + + randomStream = PwStreamCipherFactory.getInstance(header.innerRandomStream, header.protectedStreamKey); + + // TODO: Probably good to add a special note here for Arc4 mode + if ( randomStream == null ) { + throw new IOException("Protected stream type not supported."); + } + + try { + ReadXmlStreamed(decompressed); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + /* // TODO: Measure whether this buffer is better or worse for performance BufferedInputStream bis2 = new BufferedInputStream(decompressed); @@ -124,18 +159,15 @@ public class ImporterV4 extends Importer { throw new IOException("Couldn't create document builder."); } + + Document doc; try { doc = docB.parse(bis2); } catch (SAXException e) { throw new IOException("Failed to parse db xml: " + e.getLocalizedMessage()); } - - try { - db.parseDB(doc); - } catch (InconsistentDBException e) { - throw new IOException(e.getLocalizedMessage()); - } + */ /* FileOutputStream fos = new FileOutputStream("/sdcard/outputx.xml"); @@ -151,6 +183,800 @@ public class ImporterV4 extends Importer { } + + private enum KdbContext { + Null, + KeePassFile, + Meta, + Root, + MemoryProtection, + CustomIcons, + CustomIcon, + CustomData, + CustomDataItem, + RootDeletedObjects, + DeletedObject, + Group, + GroupTimes, + Entry, + EntryTimes, + EntryString, + EntryBinary, + EntryAutoType, + EntryAutoTypeItem, + EntryHistory + } + + private static final String ElemDocNode = "KeePassFile"; + private static final String ElemMeta = "Meta"; + private static final String ElemRoot = "Root"; + private static final String ElemGroup = "Group"; + private static final String ElemEntry = "Entry"; + + private static final String ElemGenerator = "Generator"; + private static final String ElemDbName = "DatabaseName"; + private static final String ElemDbNameChanged = "DatabaseNameChanged"; + private static final String ElemDbDesc = "DatabaseDescription"; + private static final String ElemDbDescChanged = "DatabaseDescriptionChanged"; + private static final String ElemDbDefaultUser = "DefaultUserName"; + private static final String ElemDbDefaultUserChanged = "DefaultUserNameChanged"; + private static final String ElemDbMntncHistoryDays = "MaintenanceHistoryDays"; + private static final String ElemRecycleBinEnabled = "RecycleBinEnabled"; + private static final String ElemRecycleBinUuid = "RecycleBinUUID"; + private static final String ElemRecycleBinChanged = "RecycleBinChanged"; + private static final String ElemEntryTemplatesGroup = "EntryTemplatesGroup"; + private static final String ElemEntryTemplatesGroupChanged = "EntryTemplatesGroupChanged"; + private static final String ElemLastSelectedGroup = "LastSelectedGroup"; + private static final String ElemLastTopVisibleGroup = "LastTopVisibleGroup"; + + private static final String ElemMemoryProt = "MemoryProtection"; + private static final String ElemProtTitle = "ProtectTitle"; + private static final String ElemProtUserName = "ProtectUserName"; + private static final String ElemProtPassword = "ProtectPassword"; + private static final String ElemProtURL = "ProtectURL"; + private static final String ElemProtNotes = "ProtectNotes"; + private static final String ElemProtAutoHide = "AutoEnableVisualHiding"; + + private static final String ElemCustomIcons = "CustomIcons"; + private static final String ElemCustomIconItem = "Icon"; + private static final String ElemCustomIconItemID = "UUID"; + private static final String ElemCustomIconItemData = "Data"; + + private static final String ElemAutoType = "AutoType"; + private static final String ElemHistory = "History"; + + private static final String ElemName = "Name"; + private static final String ElemNotes = "Notes"; + private static final String ElemUuid = "UUID"; + private static final String ElemIcon = "IconID"; + private static final String ElemCustomIconID = "CustomIconUUID"; + private static final String ElemFgColor = "ForegroundColor"; + private static final String ElemBgColor = "BackgroundColor"; + private static final String ElemOverrideUrl = "OverrideURL"; + private static final String ElemTimes = "Times"; + + private static final String ElemCreationTime = "CreationTime"; + private static final String ElemLastModTime = "LastModificationTime"; + private static final String ElemLastAccessTime = "LastAccessTime"; + private static final String ElemExpiryTime = "ExpiryTime"; + private static final String ElemExpires = "Expires"; + private static final String ElemUsageCount = "UsageCount"; + private static final String ElemLocationChanged = "LocationChanged"; + + private static final String ElemGroupDefaultAutoTypeSeq = "DefaultAutoTypeSequence"; + private static final String ElemEnableAutoType = "EnableAutoType"; + private static final String ElemEnableSearching = "EnableSearching"; + + private static final String ElemString = "String"; + private static final String ElemBinary = "Binary"; + private static final String ElemKey = "Key"; + private static final String ElemValue = "Value"; + + private static final String ElemAutoTypeEnabled = "Enabled"; + private static final String ElemAutoTypeObfuscation = "DataTransferObfuscation"; + private static final String ElemAutoTypeDefaultSeq = "DefaultSequence"; + private static final String ElemAutoTypeItem = "Association"; + private static final String ElemWindow = "Window"; + private static final String ElemKeystrokeSequence = "KeystrokeSequence"; + + private static final String AttrProtected = "Protected"; + + private static final String ElemIsExpanded = "IsExpanded"; + private static final String ElemLastTopVisibleEntry = "LastTopVisibleEntry"; + + private static final String ElemDeletedObjects = "DeletedObjects"; + private static final String ElemDeletedObject = "DeletedObject"; + private static final String ElemDeletionTime = "DeletionTime"; + + private static final String ValFalse = "False"; + private static final String ValTrue = "True"; + + private static final String ElemCustomData = "CustomData"; + private static final String ElemStringDictExItem = "Item"; + + private static final long DEFAULT_HISTORY_DAYS = 365; + + + private boolean readNextNode = true; + private Stack ctxGroups = new Stack(); + private PwGroupV4 ctxGroup = null; + private PwEntryV4 ctxEntry = null; + private String ctxStringName = null; + private String ctxStringValue = null; + private String ctxBinaryName = null; + private byte[] ctxBinaryValue = null; + private String ctxATName = null; + private String ctxATSeq = null; + private boolean entryInHistory = false; + private PwEntryV4 ctxHistoryBase = null; + private PwDeletedObject ctxDeletedObject = null; + private UUID customIconID = PwDatabaseV4.UUID_ZERO; + private byte[] customIconData; + private String customDataKey = null; + private String customDataValue = null; + + private void ReadXmlStreamed(InputStream readerStream) throws IOException { + + try { + ReadDocumentStreamed(CreatePullParser(readerStream)); + } catch (XmlPullParserException e) { + e.printStackTrace(); + throw new IOException(e.getLocalizedMessage()); + } + } + + private static XmlPullParser CreatePullParser(InputStream readerStream) throws XmlPullParserException { + XmlPullParserFactory xppf = XmlPullParserFactory.newInstance(); + xppf.setNamespaceAware(false); + + XmlPullParser xpp = xppf.newPullParser(); + xpp.setInput(readerStream, null); + + return xpp; + } + + private void ReadDocumentStreamed(XmlPullParser xpp) throws XmlPullParserException, IOException { + + ctxGroups.clear(); + + KdbContext ctx = KdbContext.Null; + + readNextNode = true; + + while (true) { + if ( readNextNode ) { + if( xpp.next() == XmlPullParser.END_DOCUMENT ) break; + } else { + readNextNode = true; + } + + switch ( xpp.getEventType() ) { + case XmlPullParser.START_TAG: + ctx = ReadXmlElement(ctx, xpp); + break; + + case XmlPullParser.END_TAG: + ctx = EndXmlElement(ctx, xpp); + break; + + default: + assert(false); + break; + + } + + } + + // Error checks + if ( ctx != KdbContext.Null ) throw new IOException("Malformed"); + if ( ctxGroups.size() != 0 ) throw new IOException("Malformed"); + } + private KdbContext ReadXmlElement(KdbContext ctx, XmlPullParser xpp) throws XmlPullParserException, IOException { + String name = xpp.getName(); + switch (ctx) { + case Null: + if ( name.equalsIgnoreCase(ElemDocNode) ) { + return SwitchContext(ctx, KdbContext.KeePassFile, xpp); + } else ReadUnknown(xpp); + break; + + case KeePassFile: + if ( name.equalsIgnoreCase(ElemMeta) ) { + return SwitchContext(ctx, KdbContext.Meta, xpp); + } else if ( name.equalsIgnoreCase(ElemRoot) ) { + return SwitchContext(ctx, KdbContext.Root, xpp); + } else { + ReadUnknown(xpp); + } + break; + + case Meta: + if ( name.equalsIgnoreCase(ElemGenerator) ) { + ReadString(xpp); // Ignore + } else if ( name.equalsIgnoreCase(ElemDbName) ) { + db.name = ReadString(xpp); + } else if ( name.equalsIgnoreCase(ElemDbNameChanged) ) { + db.nameChanged = ReadTime(xpp); + } else if ( name.equalsIgnoreCase(ElemDbDesc) ) { + db.description = ReadString(xpp); + } else if ( name.equalsIgnoreCase(ElemDbDescChanged) ) { + db.descriptionChanged = ReadTime(xpp); + } else if ( name.equalsIgnoreCase(ElemDbDefaultUser) ) { + db.defaultUserName = ReadString(xpp); + } else if ( name.equalsIgnoreCase(ElemDbDefaultUserChanged) ) { + db.defaultUserNameChanged = ReadTime(xpp); + } else if ( name.equalsIgnoreCase(ElemDbMntncHistoryDays) ) { + db.maintenanceHistoryDays = ReadUInt(xpp, DEFAULT_HISTORY_DAYS); + } else if ( name.equalsIgnoreCase(ElemMemoryProt) ) { + return SwitchContext(ctx, KdbContext.MemoryProtection, xpp); + } else if ( name.equalsIgnoreCase(ElemCustomIcons) ) { + return SwitchContext(ctx, KdbContext.CustomIcons, xpp); + } else if ( name.equalsIgnoreCase(ElemRecycleBinEnabled) ) { + db.recycleBinEnabled = ReadBool(xpp, true); + } else if ( name.equalsIgnoreCase(ElemRecycleBinUuid) ) { + db.recycleBinUUID = ReadUuid(xpp); + } else if ( name.equalsIgnoreCase(ElemRecycleBinChanged) ) { + db.recycleBinChanged = ReadTime(xpp); + } else if ( name.equalsIgnoreCase(ElemEntryTemplatesGroup) ) { + db.entryTemplatesGroup = ReadUuid(xpp); + } else if ( name.equalsIgnoreCase(ElemEntryTemplatesGroupChanged) ) { + db.entryTemplatesGroupChanged = ReadTime(xpp); + } else if ( name.equalsIgnoreCase(ElemLastSelectedGroup) ) { + db.lastSelectedGroup = ReadUuid(xpp); + } else if ( name.equalsIgnoreCase(ElemLastTopVisibleGroup) ) { + db.lastTopVisibleGroup = ReadUuid(xpp); + } else if ( name.equalsIgnoreCase(ElemCustomData) ) { + return SwitchContext(ctx, KdbContext.CustomData, xpp); + } + break; + + case MemoryProtection: + if ( name.equalsIgnoreCase(ElemProtTitle) ) { + db.memoryProtection.protectTitle = ReadBool(xpp, false); + } else if ( name.equalsIgnoreCase(ElemProtUserName) ) { + db.memoryProtection.protectUserName = ReadBool(xpp, false); + } else if ( name.equalsIgnoreCase(ElemProtPassword) ) { + db.memoryProtection.protectPassword = ReadBool(xpp, false); + } else if ( name.equalsIgnoreCase(ElemProtURL) ) { + db.memoryProtection.protectUrl = ReadBool(xpp, false); + } else if ( name.equalsIgnoreCase(ElemProtNotes) ) { + db.memoryProtection.protectNotes = ReadBool(xpp, false); + } else if ( name.equalsIgnoreCase(ElemProtAutoHide) ) { + db.memoryProtection.autoEnableVisualHiding = ReadBool(xpp, false); + } else { + ReadUnknown(xpp); + } + break; + + case CustomIcons: + if ( name.equalsIgnoreCase(ElemCustomIconItem) ) { + return SwitchContext(ctx, KdbContext.CustomIcon, xpp); + } else { + ReadUnknown(xpp); + } + break; + + case CustomIcon: + if ( name.equalsIgnoreCase(ElemCustomIconItemID) ) { + customIconID = ReadUuid(xpp); + } else if ( name.equalsIgnoreCase(ElemCustomIconItemData) ) { + String strData = ReadString(xpp); + if ( strData != null && strData.length() > 0 ) { + customIconData = Base64Coder.decode(strData); + } else { + assert(false); + } + } else { + ReadUnknown(xpp); + } + break; + + case CustomData: + if ( name.equalsIgnoreCase(ElemStringDictExItem) ) { + return SwitchContext(ctx, KdbContext.CustomDataItem, xpp); + } else { + ReadUnknown(xpp); + } + break; + + case CustomDataItem: + if ( name.equalsIgnoreCase(ElemKey) ) { + customDataKey = ReadString(xpp); + } else if ( name.equalsIgnoreCase(ElemValue) ) { + customDataValue = ReadString(xpp); + } else { + ReadUnknown(xpp); + } + break; + + case Root: + if ( name.equalsIgnoreCase(ElemGroup) ) { + assert(ctxGroups.size() == 0); + if ( ctxGroups.size() != 0 ) throw new IOException("Group list should be empty."); + + db.rootGroup = new PwGroupV4(); + ctxGroups.push(db.rootGroup); + ctxGroup = ctxGroups.peek(); + + return SwitchContext(ctx, KdbContext.Group, xpp); + } else if ( name.equalsIgnoreCase(ElemDeletedObjects) ) { + return SwitchContext(ctx, KdbContext.RootDeletedObjects, xpp); + } else { + ReadUnknown(xpp); + } + break; + + case Group: + if ( name.equalsIgnoreCase(ElemUuid) ) { + ctxGroup.uuid = ReadUuid(xpp); + } else if ( name.equalsIgnoreCase(ElemName) ) { + ctxGroup.name = ReadString(xpp); + } else if ( name.equalsIgnoreCase(ElemNotes) ) { + ctxGroup.notes = ReadString(xpp); + } else if ( name.equalsIgnoreCase(ElemIcon) ) { + ctxGroup.iconId = (int)ReadUInt(xpp, 0); + } else if ( name.equalsIgnoreCase(ElemCustomIconID) ) { + ctxGroup.customIconUuid = ReadUuid(xpp); + } else if ( name.equalsIgnoreCase(ElemTimes) ) { + return SwitchContext(ctx, KdbContext.GroupTimes, xpp); + } else if ( name.equalsIgnoreCase(ElemIsExpanded) ) { + ctxGroup.isExpanded = ReadBool(xpp, true); + } else if ( name.equalsIgnoreCase(ElemGroupDefaultAutoTypeSeq) ) { + ctxGroup.defaultAutoTypeSequence = ReadString(xpp); + } else if ( name.equalsIgnoreCase(ElemEnableAutoType) ) { + ctxGroup.enableAutoType = StringToBoolean(ReadString(xpp)); + } else if ( name.equalsIgnoreCase(ElemEnableSearching) ) { + ctxGroup.enableSearching = StringToBoolean(ReadString(xpp)); + } else if ( name.equalsIgnoreCase(ElemLastTopVisibleEntry) ) { + ctxGroup.lastTopVisibleEntry = ReadUuid(xpp); + } else if ( name.equalsIgnoreCase(ElemGroup) ) { + ctxGroup = new PwGroupV4(); + ctxGroups.peek().AddGroup(ctxGroup, true); + ctxGroups.push(ctxGroup); + + return SwitchContext(ctx, KdbContext.Group, xpp); + } else if ( name.equalsIgnoreCase(ElemEntry) ) { + ctxEntry = new PwEntryV4(); + ctxGroup.AddEntry(ctxEntry, true); + + entryInHistory = false; + return SwitchContext(ctx, KdbContext.Entry, xpp); + } else { + ReadUnknown(xpp); + } + break; + + case Entry: + if ( name.equalsIgnoreCase(ElemUuid) ) { + ctxEntry.setUUID(ReadUuid(xpp)); + } else if ( name.equalsIgnoreCase(ElemIcon) ) { + ctxEntry.imageId = (int)ReadUInt(xpp, 0); + } else if ( name.equalsIgnoreCase(ElemCustomIconID) ) { + ctxEntry.customIconUuid = ReadUuid(xpp); + } else if ( name.equalsIgnoreCase(ElemFgColor) ) { + ctxEntry.foregroundColor = ReadString(xpp); + } else if ( name.equalsIgnoreCase(ElemBgColor) ) { + ctxEntry.backgroupColor = ReadString(xpp); + } else if ( name.equalsIgnoreCase(ElemOverrideUrl) ) { + ctxEntry.overrideURL = ReadString(xpp); + } else if ( name.equalsIgnoreCase(ElemTimes) ) { + return SwitchContext(ctx, KdbContext.EntryTimes, xpp); + } else if ( name.equalsIgnoreCase(ElemString) ) { + return SwitchContext(ctx, KdbContext.EntryString, xpp); + } else if ( name.equalsIgnoreCase(ElemBinary) ) { + return SwitchContext(ctx, KdbContext.EntryBinary, xpp); + } else if ( name.equalsIgnoreCase(ElemAutoType) ) { + return SwitchContext(ctx, KdbContext.EntryAutoType, xpp); + } else if ( name.equalsIgnoreCase(ElemHistory) ) { + assert(!entryInHistory); + + if ( ! entryInHistory ) { + ctxHistoryBase = ctxEntry; + return SwitchContext(ctx, KdbContext.EntryHistory, xpp); + } else { + ReadUnknown(xpp); + } + } else { + ReadUnknown(xpp); + } + break; + + case GroupTimes: + case EntryTimes: + ITimeLogger tl; + if ( ctx == KdbContext.GroupTimes ) { + tl = ctxGroup; + } else { + tl = ctxEntry; + } + + if ( name.equalsIgnoreCase(ElemLastModTime) ) { + tl.setLastModificationTime(ReadTime(xpp)); + } else if ( name.equalsIgnoreCase(ElemCreationTime) ) { + tl.setCreationTime(ReadTime(xpp)); + } else if ( name.equalsIgnoreCase(ElemLastAccessTime) ) { + tl.setLastAccessTime(ReadTime(xpp)); + } else if ( name.equalsIgnoreCase(ElemExpiryTime) ) { + tl.setExpiryTime(ReadTime(xpp)); + } else if ( name.equalsIgnoreCase(ElemExpires) ) { + tl.setExpires(ReadBool(xpp, false)); + } else if ( name.equalsIgnoreCase(ElemUsageCount) ) { + tl.setUsageCount(ReadULong(xpp, 0)); + } else if ( name.equalsIgnoreCase(ElemLocationChanged) ) { + tl.setLocationChanged(ReadTime(xpp)); + } else { + ReadUnknown(xpp); + } + break; + + case EntryString: + if ( name.equalsIgnoreCase(ElemKey) ) { + ctxStringName = ReadString(xpp); + } else if ( name.equalsIgnoreCase(ElemValue) ) { + ctxStringValue = ReadProtectedString(xpp); + } else { + ReadUnknown(xpp); + } + break; + + case EntryBinary: + if ( name.equalsIgnoreCase(ElemKey) ) { + ctxBinaryName = ReadString(xpp); + } else if ( name.equalsIgnoreCase(ElemValue) ) { + ctxBinaryValue = ReadProtectedBinary(xpp); + } + break; + + case EntryAutoType: + if ( name.equalsIgnoreCase(ElemAutoTypeEnabled) ) { + ctxEntry.autoType.enabled = ReadBool(xpp, true); + } else if ( name.equalsIgnoreCase(ElemAutoTypeObfuscation) ) { + ctxEntry.autoType.obfuscationOptions = ReadUInt(xpp, 0); + } else if ( name.equalsIgnoreCase(ElemAutoTypeDefaultSeq) ) { + ctxEntry.autoType.defaultSequence = ReadString(xpp); + } else if ( name.equalsIgnoreCase(ElemAutoTypeItem) ) { + return SwitchContext(ctx, KdbContext.EntryAutoTypeItem, xpp); + } else { + ReadUnknown(xpp); + } + break; + + case EntryAutoTypeItem: + if ( name.equalsIgnoreCase(ElemWindow) ) { + ctxATName = ReadString(xpp); + } else if ( name.equalsIgnoreCase(ElemKeystrokeSequence) ) { + ctxATSeq = ReadString(xpp); + } else { + ReadUnknown(xpp); + } + break; + + case EntryHistory: + if ( name.equalsIgnoreCase(ElemEntry) ) { + ctxEntry = new PwEntryV4(); + ctxHistoryBase.history.add(ctxEntry); + + entryInHistory = true; + return SwitchContext(ctx, KdbContext.Entry, xpp); + } else { + ReadUnknown(xpp); + } + break; + + case RootDeletedObjects: + if ( name.equalsIgnoreCase(ElemDeletedObject) ) { + ctxDeletedObject = new PwDeletedObject(); + db.deletedObjects.add(ctxDeletedObject); + + return SwitchContext(ctx, KdbContext.DeletedObject, xpp); + } else { + ReadUnknown(xpp); + } + break; + + case DeletedObject: + if ( name.equalsIgnoreCase(ElemUuid) ) { + ctxDeletedObject.uuid = ReadUuid(xpp); + } else if ( name.equalsIgnoreCase(ElemDeletionTime) ) { + ctxDeletedObject.setDeletionTime(ReadTime(xpp)); + } else { + ReadUnknown(xpp); + } + break; + + default: + ReadUnknown(xpp); + break; + } + + return ctx; + } + + private KdbContext EndXmlElement(KdbContext ctx, XmlPullParser xpp) throws XmlPullParserException { + assert(xpp.getEventType() == XmlPullParser.END_TAG); + + String name = xpp.getName(); + if ( ctx == KdbContext.KeePassFile && name.equalsIgnoreCase(ElemDocNode) ) { + return KdbContext.Null; + } else if ( ctx == KdbContext.Meta && name.equalsIgnoreCase(ElemMeta) ) { + return KdbContext.KeePassFile; + } else if ( ctx == KdbContext.Root && name.equalsIgnoreCase(ElemRoot) ) { + return KdbContext.KeePassFile; + } else if ( ctx == KdbContext.MemoryProtection && name.equalsIgnoreCase(ElemMemoryProt) ) { + return KdbContext.Meta; + } else if ( ctx == KdbContext.CustomIcons && name.equalsIgnoreCase(ElemCustomIcons) ) { + return KdbContext.Meta; + } else if ( ctx == KdbContext.CustomIcon && name.equalsIgnoreCase(ElemCustomIconItem) ) { + if ( ! customIconID.equals(PwDatabaseV4.UUID_ZERO) ) { + db.customIcons.add(new PwCustomIcon(customIconID, customIconData)); + } else assert(false); + + customIconID = PwDatabaseV4.UUID_ZERO; + customIconData = null; + + return KdbContext.CustomIcons; + } else if ( ctx == KdbContext.CustomData && name.equalsIgnoreCase(ElemCustomData) ) { + return KdbContext.Meta; + } else if ( ctx == KdbContext.CustomDataItem && name.equalsIgnoreCase(ElemStringDictExItem) ) { + if ( customDataKey != null && customDataValue != null) { + db.customData.put(customDataKey, customDataValue); + } else assert(false); + + customDataKey = null; + customDataValue = null; + + return KdbContext.CustomData; + } else if ( ctx == KdbContext.Group && name.equalsIgnoreCase(ElemGroup) ) { + if ( ctxGroup.uuid == null || ctxGroup.uuid.equals(PwDatabaseV4.UUID_ZERO) ) { + ctxGroup.uuid = UUID.randomUUID(); + } + + ctxGroups.pop(); + + if ( ctxGroups.size() == 0 ) { + ctxGroup = null; + return KdbContext.Root; + } else { + ctxGroup = ctxGroups.peek(); + return KdbContext.Group; + } + } else if ( ctx == KdbContext.GroupTimes && name.equalsIgnoreCase(ElemTimes) ) { + return KdbContext.Group; + } else if ( ctx == KdbContext.Entry && name.equalsIgnoreCase(ElemEntry) ) { + if ( ctxEntry.uuid == null || ctxEntry.uuid.equals(PwDatabaseV4.UUID_ZERO) ) { + ctxEntry.uuid = UUID.randomUUID(); + } + + if ( entryInHistory ) { + ctxEntry = ctxHistoryBase; + return KdbContext.EntryHistory; + } + + return KdbContext.Group; + } else if ( ctx == KdbContext.EntryTimes && name.equalsIgnoreCase(ElemTimes) ) { + return KdbContext.Entry; + } else if ( ctx == KdbContext.EntryString && name.equalsIgnoreCase(ElemString) ) { + ctxEntry.strings.put(ctxStringName, ctxStringValue); + ctxStringName = null; + ctxStringValue = null; + + return KdbContext.Entry; + } else if ( ctx == KdbContext.EntryBinary && name.equalsIgnoreCase(ElemBinary) ) { + ctxEntry.binaries.put(ctxBinaryName, ctxBinaryValue); + ctxBinaryName = null; + ctxBinaryValue = null; + + return KdbContext.Entry; + } else if ( ctx == KdbContext.EntryAutoType && name.equalsIgnoreCase(ElemAutoType) ) { + return KdbContext.Entry; + } else if ( ctx == KdbContext.EntryAutoTypeItem && name.equalsIgnoreCase(ElemAutoTypeItem) ) { + ctxEntry.autoType.put(ctxATName, ctxATSeq); + ctxATName = null; + ctxATSeq = null; + + return KdbContext.EntryAutoType; + } else if ( ctx == KdbContext.EntryHistory && name.equalsIgnoreCase(ElemHistory) ) { + entryInHistory = false; + return KdbContext.Entry; + } else if ( ctx == KdbContext.RootDeletedObjects && name.equalsIgnoreCase(ElemDeletedObjects) ) { + return KdbContext.Root; + } else if ( ctx == KdbContext.DeletedObject && name.equalsIgnoreCase(ElemDeletedObject) ) { + ctxDeletedObject = null; + return KdbContext.RootDeletedObjects; + } else { + assert(false); + + throw new RuntimeException("Invalid end element"); + } + } + + private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-mm-dd'T'HH:mm:ss'Z'"); + private Date ReadTime(XmlPullParser xpp) throws IOException, XmlPullParserException { + String sDate = ReadString(xpp); + + Date utcDate; + try { + utcDate = dateFormat.parse(sDate); + } catch (ParseException e) { + e.printStackTrace(); + throw new IOException(e.getLocalizedMessage()); + } + + return utcDate; + + } + + private void ReadUnknown(XmlPullParser xpp) throws XmlPullParserException, IOException { + assert(false); + + if ( xpp.isEmptyElementTag() ) return; + + String unknownName = xpp.getName(); + ProcessNode(xpp); + + while (xpp.next() != XmlPullParser.END_DOCUMENT ) { + if ( xpp.getEventType() == XmlPullParser.END_TAG ) break; + if ( xpp.getEventType() == XmlPullParser.START_TAG ) continue; + + ReadUnknown(xpp); + } + + assert(xpp.getName() == unknownName); + + } + + private boolean ReadBool(XmlPullParser xpp, boolean bDefault) throws IOException, XmlPullParserException { + String str = ReadString(xpp); + + if ( str.equalsIgnoreCase("true") ) { + return true; + } else if ( str.equalsIgnoreCase("false") ) { + return false; + } else { + return bDefault; + } + } + + private UUID ReadUuid(XmlPullParser xpp) throws IOException, XmlPullParserException { + String encoded = ReadString(xpp); + + if (encoded == null || encoded.length() == 0 ) { + return PwDatabaseV4.UUID_ZERO; + } + + // TODO: Switch to framework Base64 once API level 8 is the minimum + byte[] buf = Base64Coder.decode(encoded); + + return Types.bytestoUUID(buf); + } + + private static final long MAX_UINT = 4294967296L; // 2^32 + private long ReadUInt(XmlPullParser xpp, long uDefault) throws IOException, XmlPullParserException { + long u; + + u = ReadULong(xpp, uDefault); + if ( u < 0 || u > MAX_UINT ) { + throw new NumberFormatException("Outside of the uint size"); + } + + return u; + + } + + private long ReadULong(XmlPullParser xpp, long uDefault) throws IOException, XmlPullParserException { + String str = ReadString(xpp); + + long u; + try { + u = Long.parseLong(str); + } catch( NumberFormatException e) { + u = uDefault; + } + + return u; + + } + + private String ReadProtectedString(XmlPullParser xpp) throws XmlPullParserException, IOException { + byte[] buf = ProcessNode(xpp); + + if ( buf != null) { + try { + return new String(buf, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + throw new IOException(e.getLocalizedMessage()); + } + } + + return ReadString(xpp); + } + + private byte[] ReadProtectedBinary(XmlPullParser xpp) throws XmlPullParserException, IOException { + byte[] buf = ProcessNode(xpp); + + if ( buf != null ) return buf; + + String base64 = ReadString(xpp); + if ( base64.length() == 0 ) return new byte[0]; + + return Base64Coder.decode(base64); + } + + private String ReadString(XmlPullParser xpp) throws IOException, XmlPullParserException { + byte[] buf = ProcessNode(xpp); + + if ( buf != null ) { + try { + return new String(buf, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IOException(e.getLocalizedMessage()); + } + } + + //readNextNode = false; + return xpp.nextText(); + + } + + private String ReadStringRaw(XmlPullParser xpp) throws XmlPullParserException, IOException { + + //readNextNode = false; + return xpp.nextText(); + } + + private byte[] ProcessNode(XmlPullParser xpp) throws XmlPullParserException, IOException { + assert(xpp.getEventType() == XmlPullParser.START_TAG); + + byte[] buf = null; + + if ( xpp.getAttributeCount() > 0 ) { + String protect = xpp.getAttributeValue(null, AttrProtected); + if ( protect != null && protect.equalsIgnoreCase(ValTrue) ) { + String encrypted = ReadStringRaw(xpp); + + if ( encrypted.length() > 0 ) { + buf = Base64Coder.decode(encrypted); + byte[] plainText = new byte[buf.length]; + + randomStream.processBytes(buf, 0, buf.length, plainText, 0); + + return plainText; + } else { + buf = new byte[0]; + } + } + } + + return buf; + } + + private KdbContext SwitchContext(KdbContext ctxCurrent, KdbContext ctxNew, + XmlPullParser xpp) throws XmlPullParserException, IOException { + + if ( xpp.isEmptyElementTag() ) { + xpp.next(); // Consume the end tag + return ctxCurrent; + } + return ctxNew; + } + + + private Boolean StringToBoolean(String str) { + if ( str == null || str.length() == 0 ) { + return null; + } + + String trimmed = str.trim(); + if ( trimmed.equalsIgnoreCase("true") ) { + return true; + } else if ( trimmed.equalsIgnoreCase("false") ) { + return false; + } + + return null; + + } } diff --git a/src/com/keepassdroid/database/save/PwEntryOutputV3.java b/src/com/keepassdroid/database/save/PwEntryOutputV3.java index 825d6d43e..0033f6652 100644 --- a/src/com/keepassdroid/database/save/PwEntryOutputV3.java +++ b/src/com/keepassdroid/database/save/PwEntryOutputV3.java @@ -74,7 +74,7 @@ public class PwEntryOutputV3 { // UUID mOS.write(UUID_FIELD_TYPE); mOS.write(UUID_FIELD_SIZE); - mOS.write(mPE.uuid); + mOS.write(Types.UUIDtoBytes(mPE.getUUID())); // Group ID mOS.write(GROUPID_FIELD_TYPE); diff --git a/src/com/keepassdroid/search/SearchDbHelper.java b/src/com/keepassdroid/search/SearchDbHelper.java index 8fff8f7e5..cfac80469 100644 --- a/src/com/keepassdroid/search/SearchDbHelper.java +++ b/src/com/keepassdroid/search/SearchDbHelper.java @@ -32,10 +32,8 @@ import android.util.Log; import com.keepassdroid.Database; import com.keepassdroid.database.PwEntry; -import com.keepassdroid.database.PwEntryV3; import com.keepassdroid.database.PwGroup; import com.keepassdroid.database.PwGroupV3; -import com.keepassdroid.utils.Types; public class SearchDbHelper { private static final String DATABASE_NAME = "search"; @@ -100,7 +98,7 @@ public class SearchDbHelper { private ContentValues buildNewEntryContent(PwEntry entry) { ContentValues cv = new ContentValues(); - UUID uuid = Types.bytestoUUID(entry.uuid); + UUID uuid = entry.getUUID(); String uuidStr = uuid.toString(); cv.put(KEY_UUID, uuidStr); @@ -137,7 +135,7 @@ public class SearchDbHelper { } public void deleteEntry(PwEntry entry) { - UUID uuid = Types.bytestoUUID(entry.uuid); + UUID uuid = entry.getUUID(); String uuidStr = uuid.toString(); mDb.delete(SEARCH_TABLE, KEY_UUID + " = ?", new String[] {uuidStr}); @@ -157,7 +155,7 @@ public class SearchDbHelper { String sUUID = cursor.getString(0); UUID uuid = UUID.fromString(sUUID); Log.d("TAG", uuid.toString()); - PwEntryV3 entry = (PwEntryV3) db.entries.get(uuid).get(); + PwEntry entry = (PwEntry) db.entries.get(uuid).get(); group.childEntries.add(entry); cursor.moveToNext(); diff --git a/src/com/keepassdroid/utils/Types.java b/src/com/keepassdroid/utils/Types.java index df1b41b51..6904efba9 100644 --- a/src/com/keepassdroid/utils/Types.java +++ b/src/com/keepassdroid/utils/Types.java @@ -178,19 +178,31 @@ public class Types { } public static UUID bytestoUUID(byte[] buf) { - - long msb = 0; - for (int i = 0; i < 8; i++) { - msb = (msb << 8) | (buf[i] & 0xff); - } - + return bytestoUUID(buf, 0); + } + + public static UUID bytestoUUID(byte[] buf, int offset) { long lsb = 0; - for (int i = 8; i < 16; i++) { - lsb = (lsb << 8) | (buf[i] & 0xff); + for (int i = 15; i >= 8; i--) { + lsb = (lsb << 8) | (buf[i + offset] & 0xff); + } + + long msb = 0; + for (int i = 7; i >= 0; i--) { + msb = (msb << 8) | (buf[i + offset] & 0xff); } return new UUID(msb, lsb); } - + + public static byte[] UUIDtoBytes(UUID uuid) { + byte[] buf = new byte[16]; + + LEDataOutputStream.writeLong(uuid.getMostSignificantBits(), buf, 0); + LEDataOutputStream.writeLong(uuid.getLeastSignificantBits(), buf, 8); + + return buf; + } + } diff --git a/src/org/bouncycastle/crypto/CipherParameters.java b/src/org/bouncycastle/crypto/CipherParameters.java new file mode 100644 index 000000000..5be873047 --- /dev/null +++ b/src/org/bouncycastle/crypto/CipherParameters.java @@ -0,0 +1,8 @@ +package org.bouncycastle.crypto; + +/** + * all parameter classes implement this. + */ +public interface CipherParameters +{ +} diff --git a/src/org/bouncycastle/crypto/DataLengthException.java b/src/org/bouncycastle/crypto/DataLengthException.java new file mode 100644 index 000000000..29b37ae41 --- /dev/null +++ b/src/org/bouncycastle/crypto/DataLengthException.java @@ -0,0 +1,30 @@ +package org.bouncycastle.crypto; + +/** + * this exception is thrown if a buffer that is meant to have output + * copied into it turns out to be too short, or if we've been given + * insufficient input. In general this exception will get thrown rather + * than an ArrayOutOfBounds exception. + */ +@SuppressWarnings("serial") +public class DataLengthException + extends RuntimeCryptoException +{ + /** + * base constructor. + */ + public DataLengthException() + { + } + + /** + * create a DataLengthException with the given message. + * + * @param message the message to be carried with the exception. + */ + public DataLengthException( + String message) + { + super(message); + } +} diff --git a/src/org/bouncycastle/crypto/MaxBytesExceededException.java b/src/org/bouncycastle/crypto/MaxBytesExceededException.java new file mode 100644 index 000000000..5747f27c5 --- /dev/null +++ b/src/org/bouncycastle/crypto/MaxBytesExceededException.java @@ -0,0 +1,28 @@ +package org.bouncycastle.crypto; + +/** + * this exception is thrown whenever a cipher requires a change of key, iv + * or similar after x amount of bytes enciphered + */ +@SuppressWarnings("serial") +public class MaxBytesExceededException + extends RuntimeCryptoException +{ + /** + * base constructor. + */ + public MaxBytesExceededException() + { + } + + /** + * create an with the given message. + * + * @param message the message to be carried with the exception. + */ + public MaxBytesExceededException( + String message) + { + super(message); + } +} diff --git a/src/org/bouncycastle/crypto/RuntimeCryptoException.java b/src/org/bouncycastle/crypto/RuntimeCryptoException.java new file mode 100644 index 000000000..8e782dee1 --- /dev/null +++ b/src/org/bouncycastle/crypto/RuntimeCryptoException.java @@ -0,0 +1,27 @@ +package org.bouncycastle.crypto; + +/** + * the foundation class for the exceptions thrown by the crypto packages. + */ +@SuppressWarnings("serial") +public class RuntimeCryptoException + extends RuntimeException +{ + /** + * base constructor. + */ + public RuntimeCryptoException() + { + } + + /** + * create a RuntimeCryptoException with the given message. + * + * @param message the message to be carried with the exception. + */ + public RuntimeCryptoException( + String message) + { + super(message); + } +} diff --git a/src/org/bouncycastle/crypto/StreamCipher.java b/src/org/bouncycastle/crypto/StreamCipher.java new file mode 100644 index 000000000..2a55d4f65 --- /dev/null +++ b/src/org/bouncycastle/crypto/StreamCipher.java @@ -0,0 +1,53 @@ +package org.bouncycastle.crypto; + +/** + * the interface stream ciphers conform to. + */ +public interface StreamCipher +{ + /** + * Initialise the cipher. + * + * @param forEncryption if true the cipher is initialised for + * encryption, if false for decryption. + * @param params the key and other data required by the cipher. + * @exception IllegalArgumentException if the params argument is + * inappropriate. + */ + public void init(boolean forEncryption, CipherParameters params) + throws IllegalArgumentException; + + /** + * Return the name of the algorithm the cipher implements. + * + * @return the name of the algorithm the cipher implements. + */ + public String getAlgorithmName(); + + /** + * encrypt/decrypt a single byte returning the result. + * + * @param in the byte to be processed. + * @return the result of processing the input byte. + */ + public byte returnByte(byte in); + + /** + * process a block of bytes from in putting the result into out. + * + * @param in the input byte array. + * @param inOff the offset into the in array where the data to be processed starts. + * @param len the number of bytes to be processed. + * @param out the output buffer the processed bytes go into. + * @param outOff the offset into the output byte array the processed data starts at. + * @exception DataLengthException if the output buffer is too small. + */ + public void processBytes(byte[] in, int inOff, int len, byte[] out, int outOff) + throws DataLengthException; + + /** + * reset the cipher. This leaves it in the same state + * it was at after the last init (if there was one). + */ + public void reset(); +} diff --git a/src/org/bouncycastle/crypto/engines/Salsa20Engine.java b/src/org/bouncycastle/crypto/engines/Salsa20Engine.java new file mode 100644 index 000000000..c51d120b9 --- /dev/null +++ b/src/org/bouncycastle/crypto/engines/Salsa20Engine.java @@ -0,0 +1,377 @@ +package org.bouncycastle.crypto.engines; + +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.DataLengthException; +import org.bouncycastle.crypto.MaxBytesExceededException; +import org.bouncycastle.crypto.StreamCipher; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; + +/** + * Implementation of Daniel J. Bernstein's Salsa20 stream cipher, Snuffle 2005 + */ + +public class Salsa20Engine + implements StreamCipher +{ + /** Constants */ + private final static int stateSize = 16; // 16, 32 bit ints = 64 bytes + + private final static byte[] + sigma = toByteArray("expand 32-byte k"), + tau = toByteArray("expand 16-byte k"); + + // No sure why the version in org.bouncycastle.util.Strings was throwing NoSuchMethodErrors + private static byte[] toByteArray(String string) + { + byte[] bytes = new byte[string.length()]; + + for (int i = 0; i != bytes.length; i++) + { + char ch = string.charAt(i); + + bytes[i] = (byte)ch; + } + + return bytes; + } + + + /* + * variables to hold the state of the engine + * during encryption and decryption + */ + private int index = 0; + private int[] engineState = new int[stateSize]; // state + private int[] x = new int[stateSize] ; // internal buffer + private byte[] keyStream = new byte[stateSize * 4], // expanded state, 64 bytes + workingKey = null, + workingIV = null; + private boolean initialised = false; + + /* + * internal counter + */ + private int cW0, cW1, cW2; + + + /** + * initialise a Salsa20 cipher. + * + * @param forEncryption whether or not we are for encryption. + * @param params the parameters required to set up the cipher. + * @exception IllegalArgumentException if the params argument is + * inappropriate. + */ + public void init( + boolean forEncryption, + CipherParameters params) + { + /* + * Salsa20 encryption and decryption is completely + * symmetrical, so the 'forEncryption' is + * irrelevant. (Like 90% of stream ciphers) + */ + + if (!(params instanceof ParametersWithIV)) + { + throw new IllegalArgumentException("Salsa20 Init parameters must include an IV"); + } + + ParametersWithIV ivParams = (ParametersWithIV) params; + + byte[] iv = ivParams.getIV(); + + if (iv == null || iv.length != 8) + { + throw new IllegalArgumentException("Salsa20 requires exactly 8 bytes of IV"); + } + + if (!(ivParams.getParameters() instanceof KeyParameter)) + { + throw new IllegalArgumentException("Salsa20 Init parameters must include a key"); + } + + KeyParameter key = (KeyParameter) ivParams.getParameters(); + + workingKey = key.getKey(); + workingIV = iv; + + setKey(workingKey, workingIV); + } + + public String getAlgorithmName() + { + return "Salsa20"; + } + + public byte returnByte(byte in) + { + if (limitExceeded()) + { + throw new MaxBytesExceededException("2^70 byte limit per IV; Change IV"); + } + + if (index == 0) + { + salsa20WordToByte(engineState, keyStream); + engineState[8]++; + if (engineState[8] == 0) + { + engineState[9]++; + } + } + byte out = (byte)(keyStream[index]^in); + index = (index + 1) & 63; + + return out; + } + + public void processBytes( + byte[] in, + int inOff, + int len, + byte[] out, + int outOff) + { + if (!initialised) + { + throw new IllegalStateException(getAlgorithmName()+" not initialised"); + } + + if ((inOff + len) > in.length) + { + throw new DataLengthException("input buffer too short"); + } + + if ((outOff + len) > out.length) + { + throw new DataLengthException("output buffer too short"); + } + + if (limitExceeded(len)) + { + throw new MaxBytesExceededException("2^70 byte limit per IV would be exceeded; Change IV"); + } + + for (int i = 0; i < len; i++) + { + if (index == 0) + { + salsa20WordToByte(engineState, keyStream); + engineState[8]++; + if (engineState[8] == 0) + { + engineState[9]++; + } + } + out[i+outOff] = (byte)(keyStream[index]^in[i+inOff]); + index = (index + 1) & 63; + } + } + + public void reset() + { + setKey(workingKey, workingIV); + } + + // Private implementation + + private void setKey(byte[] keyBytes, byte[] ivBytes) + { + workingKey = keyBytes; + workingIV = ivBytes; + + index = 0; + resetCounter(); + int offset = 0; + byte[] constants; + + // Key + engineState[1] = byteToIntLittle(workingKey, 0); + engineState[2] = byteToIntLittle(workingKey, 4); + engineState[3] = byteToIntLittle(workingKey, 8); + engineState[4] = byteToIntLittle(workingKey, 12); + + if (workingKey.length == 32) + { + constants = sigma; + offset = 16; + } + else + { + constants = tau; + } + + engineState[11] = byteToIntLittle(workingKey, offset); + engineState[12] = byteToIntLittle(workingKey, offset+4); + engineState[13] = byteToIntLittle(workingKey, offset+8); + engineState[14] = byteToIntLittle(workingKey, offset+12); + engineState[0 ] = byteToIntLittle(constants, 0); + engineState[5 ] = byteToIntLittle(constants, 4); + engineState[10] = byteToIntLittle(constants, 8); + engineState[15] = byteToIntLittle(constants, 12); + + // IV + engineState[6] = byteToIntLittle(workingIV, 0); + engineState[7] = byteToIntLittle(workingIV, 4); + engineState[8] = engineState[9] = 0; + + initialised = true; + } + + /** + * Salsa20 function + * + * @param input input data + * + * @return keystream + */ + private void salsa20WordToByte(int[] input, byte[] output) + { + System.arraycopy(input, 0, x, 0, input.length); + + for (int i = 0; i < 10; i++) + { + x[ 4] ^= rotl((x[ 0]+x[12]), 7); + x[ 8] ^= rotl((x[ 4]+x[ 0]), 9); + x[12] ^= rotl((x[ 8]+x[ 4]),13); + x[ 0] ^= rotl((x[12]+x[ 8]),18); + x[ 9] ^= rotl((x[ 5]+x[ 1]), 7); + x[13] ^= rotl((x[ 9]+x[ 5]), 9); + x[ 1] ^= rotl((x[13]+x[ 9]),13); + x[ 5] ^= rotl((x[ 1]+x[13]),18); + x[14] ^= rotl((x[10]+x[ 6]), 7); + x[ 2] ^= rotl((x[14]+x[10]), 9); + x[ 6] ^= rotl((x[ 2]+x[14]),13); + x[10] ^= rotl((x[ 6]+x[ 2]),18); + x[ 3] ^= rotl((x[15]+x[11]), 7); + x[ 7] ^= rotl((x[ 3]+x[15]), 9); + x[11] ^= rotl((x[ 7]+x[ 3]),13); + x[15] ^= rotl((x[11]+x[ 7]),18); + x[ 1] ^= rotl((x[ 0]+x[ 3]), 7); + x[ 2] ^= rotl((x[ 1]+x[ 0]), 9); + x[ 3] ^= rotl((x[ 2]+x[ 1]),13); + x[ 0] ^= rotl((x[ 3]+x[ 2]),18); + x[ 6] ^= rotl((x[ 5]+x[ 4]), 7); + x[ 7] ^= rotl((x[ 6]+x[ 5]), 9); + x[ 4] ^= rotl((x[ 7]+x[ 6]),13); + x[ 5] ^= rotl((x[ 4]+x[ 7]),18); + x[11] ^= rotl((x[10]+x[ 9]), 7); + x[ 8] ^= rotl((x[11]+x[10]), 9); + x[ 9] ^= rotl((x[ 8]+x[11]),13); + x[10] ^= rotl((x[ 9]+x[ 8]),18); + x[12] ^= rotl((x[15]+x[14]), 7); + x[13] ^= rotl((x[12]+x[15]), 9); + x[14] ^= rotl((x[13]+x[12]),13); + x[15] ^= rotl((x[14]+x[13]),18); + } + + int offset = 0; + for (int i = 0; i < stateSize; i++) + { + intToByteLittle(x[i] + input[i], output, offset); + offset += 4; + } + + for (int i = stateSize; i < x.length; i++) + { + intToByteLittle(x[i], output, offset); + offset += 4; + } + } + + /** + * 32 bit word to 4 byte array in little endian order + * + * @param x value to 'unpack' + * + * @return value of x expressed as a byte[] array in little endian order + */ + private byte[] intToByteLittle(int x, byte[] out, int off) + { + out[off] = (byte)x; + out[off + 1] = (byte)(x >>> 8); + out[off + 2] = (byte)(x >>> 16); + out[off + 3] = (byte)(x >>> 24); + return out; + } + + /** + * Rotate left + * + * @param x value to rotate + * @param y amount to rotate x + * + * @return rotated x + */ + private int rotl(int x, int y) + { + return (x << y) | (x >>> -y); + } + + /** + * Pack byte[] array into an int in little endian order + * + * @param x byte array to 'pack' + * @param offset only x[offset]..x[offset+3] will be packed + * + * @return x[offset]..x[offset+3] 'packed' into an int in little-endian order + */ + private int byteToIntLittle(byte[] x, int offset) + { + return ((x[offset] & 255)) | + ((x[offset + 1] & 255) << 8) | + ((x[offset + 2] & 255) << 16) | + (x[offset + 3] << 24); + } + + private void resetCounter() + { + cW0 = 0; + cW1 = 0; + cW2 = 0; + } + + private boolean limitExceeded() + { + cW0++; + if (cW0 == 0) + { + cW1++; + if (cW1 == 0) + { + cW2++; + return (cW2 & 0x20) != 0; // 2^(32 + 32 + 6) + } + } + + return false; + } + + /* + * this relies on the fact len will always be positive. + */ + private boolean limitExceeded(int len) + { + if (cW0 >= 0) + { + cW0 += len; + } + else + { + cW0 += len; + if (cW0 >= 0) + { + cW1++; + if (cW1 == 0) + { + cW2++; + return (cW2 & 0x20) != 0; // 2^(32 + 32 + 6) + } + } + } + + return false; + } +} diff --git a/src/org/bouncycastle/crypto/params/KeyParameter.java b/src/org/bouncycastle/crypto/params/KeyParameter.java new file mode 100644 index 000000000..5c4fe0e0b --- /dev/null +++ b/src/org/bouncycastle/crypto/params/KeyParameter.java @@ -0,0 +1,30 @@ +package org.bouncycastle.crypto.params; + +import org.bouncycastle.crypto.CipherParameters; + +public class KeyParameter + implements CipherParameters +{ + private byte[] key; + + public KeyParameter( + byte[] key) + { + this(key, 0, key.length); + } + + public KeyParameter( + byte[] key, + int keyOff, + int keyLen) + { + this.key = new byte[keyLen]; + + System.arraycopy(key, keyOff, this.key, 0, keyLen); + } + + public byte[] getKey() + { + return key; + } +} diff --git a/src/org/bouncycastle/crypto/params/ParametersWithIV.java b/src/org/bouncycastle/crypto/params/ParametersWithIV.java new file mode 100644 index 000000000..4a1e6e9a3 --- /dev/null +++ b/src/org/bouncycastle/crypto/params/ParametersWithIV.java @@ -0,0 +1,39 @@ +package org.bouncycastle.crypto.params; + +import org.bouncycastle.crypto.CipherParameters; + +public class ParametersWithIV + implements CipherParameters +{ + private byte[] iv; + private CipherParameters parameters; + + public ParametersWithIV( + CipherParameters parameters, + byte[] iv) + { + this(parameters, iv, 0, iv.length); + } + + public ParametersWithIV( + CipherParameters parameters, + byte[] iv, + int ivOff, + int ivLen) + { + this.iv = new byte[ivLen]; + this.parameters = parameters; + + System.arraycopy(iv, ivOff, this.iv, 0, ivLen); + } + + public byte[] getIV() + { + return iv; + } + + public CipherParameters getParameters() + { + return parameters; + } +} diff --git a/src/org/bouncycastle/util/Strings.java b/src/org/bouncycastle/util/Strings.java new file mode 100644 index 000000000..253e72261 --- /dev/null +++ b/src/org/bouncycastle/util/Strings.java @@ -0,0 +1,247 @@ +package org.bouncycastle.util; + +import java.io.ByteArrayOutputStream; +import java.util.Vector; + +public final class Strings +{ + public static String fromUTF8ByteArray(byte[] bytes) + { + int i = 0; + int length = 0; + + while (i < bytes.length) + { + length++; + if ((bytes[i] & 0xf0) == 0xf0) + { + // surrogate pair + length++; + i += 4; + } + else if ((bytes[i] & 0xe0) == 0xe0) + { + i += 3; + } + else if ((bytes[i] & 0xc0) == 0xc0) + { + i += 2; + } + else + { + i += 1; + } + } + + char[] cs = new char[length]; + + i = 0; + length = 0; + + while (i < bytes.length) + { + char ch; + + if ((bytes[i] & 0xf0) == 0xf0) + { + int codePoint = ((bytes[i] & 0x03) << 18) | ((bytes[i+1] & 0x3F) << 12) | ((bytes[i+2] & 0x3F) << 6) | (bytes[i+3] & 0x3F); + int U = codePoint - 0x10000; + char W1 = (char)(0xD800 | (U >> 10)); + char W2 = (char)(0xDC00 | (U & 0x3FF)); + cs[length++] = W1; + ch = W2; + i += 4; + } + else if ((bytes[i] & 0xe0) == 0xe0) + { + ch = (char)(((bytes[i] & 0x0f) << 12) + | ((bytes[i + 1] & 0x3f) << 6) | (bytes[i + 2] & 0x3f)); + i += 3; + } + else if ((bytes[i] & 0xd0) == 0xd0) + { + ch = (char)(((bytes[i] & 0x1f) << 6) | (bytes[i + 1] & 0x3f)); + i += 2; + } + else if ((bytes[i] & 0xc0) == 0xc0) + { + ch = (char)(((bytes[i] & 0x1f) << 6) | (bytes[i + 1] & 0x3f)); + i += 2; + } + else + { + ch = (char)(bytes[i] & 0xff); + i += 1; + } + + cs[length++] = ch; + } + + return new String(cs); + } + + public static byte[] toUTF8ByteArray(String string) + { + return toUTF8ByteArray(string.toCharArray()); + } + + public static byte[] toUTF8ByteArray(char[] string) + { + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + char[] c = string; + int i = 0; + + while (i < c.length) + { + char ch = c[i]; + + if (ch < 0x0080) + { + bOut.write(ch); + } + else if (ch < 0x0800) + { + bOut.write(0xc0 | (ch >> 6)); + bOut.write(0x80 | (ch & 0x3f)); + } + // surrogate pair + else if (ch >= 0xD800 && ch <= 0xDFFF) + { + // in error - can only happen, if the Java String class has a + // bug. + if (i + 1 >= c.length) + { + throw new IllegalStateException("invalid UTF-16 codepoint"); + } + char W1 = ch; + ch = c[++i]; + char W2 = ch; + // in error - can only happen, if the Java String class has a + // bug. + if (W1 > 0xDBFF) + { + throw new IllegalStateException("invalid UTF-16 codepoint"); + } + int codePoint = (((W1 & 0x03FF) << 10) | (W2 & 0x03FF)) + 0x10000; + bOut.write(0xf0 | (codePoint >> 18)); + bOut.write(0x80 | ((codePoint >> 12) & 0x3F)); + bOut.write(0x80 | ((codePoint >> 6) & 0x3F)); + bOut.write(0x80 | (codePoint & 0x3F)); + } + else + { + bOut.write(0xe0 | (ch >> 12)); + bOut.write(0x80 | ((ch >> 6) & 0x3F)); + bOut.write(0x80 | (ch & 0x3F)); + } + + i++; + } + + return bOut.toByteArray(); + } + + /** + * A locale independent version of toUpperCase. + * + * @param string input to be converted + * @return a US Ascii uppercase version + */ + public static String toUpperCase(String string) + { + boolean changed = false; + char[] chars = string.toCharArray(); + + for (int i = 0; i != chars.length; i++) + { + char ch = chars[i]; + if ('a' <= ch && 'z' >= ch) + { + changed = true; + chars[i] = (char)(ch - 'a' + 'A'); + } + } + + if (changed) + { + return new String(chars); + } + + return string; + } + + /** + * A locale independent version of toLowerCase. + * + * @param string input to be converted + * @return a US ASCII lowercase version + */ + public static String toLowerCase(String string) + { + boolean changed = false; + char[] chars = string.toCharArray(); + + for (int i = 0; i != chars.length; i++) + { + char ch = chars[i]; + if ('A' <= ch && 'Z' >= ch) + { + changed = true; + chars[i] = (char)(ch - 'A' + 'a'); + } + } + + if (changed) + { + return new String(chars); + } + + return string; + } + + public static byte[] toByteArray(String string) + { + byte[] bytes = new byte[string.length()]; + + for (int i = 0; i != bytes.length; i++) + { + char ch = string.charAt(i); + + bytes[i] = (byte)ch; + } + + return bytes; + } + + @SuppressWarnings("unchecked") + public static String[] split(String input, char delimiter) + { + Vector v = new Vector(); + boolean moreTokens = true; + String subString; + + while (moreTokens) + { + int tokenLocation = input.indexOf(delimiter); + if (tokenLocation > 0) + { + subString = input.substring(0, tokenLocation); + v.addElement(subString); + input = input.substring(tokenLocation + 1); + } + else + { + moreTokens = false; + v.addElement(input); + } + } + + String[] res = new String[v.size()]; + + for (int i = 0; i != res.length; i++) + { + res[i] = (String)v.elementAt(i); + } + return res; + } +} diff --git a/tests/src/com/keepassdroid/tests/TypesTest.java b/tests/src/com/keepassdroid/tests/TypesTest.java index 48ef3ef68..daf09e45a 100644 --- a/tests/src/com/keepassdroid/tests/TypesTest.java +++ b/tests/src/com/keepassdroid/tests/TypesTest.java @@ -23,10 +23,10 @@ import static org.junit.Assert.assertArrayEquals; import java.util.Calendar; import java.util.Random; +import java.util.UUID; import junit.framework.TestCase; - import com.keepassdroid.database.PwDate; import com.keepassdroid.stream.LEDataInputStream; import com.keepassdroid.stream.LEDataOutputStream; @@ -180,4 +180,15 @@ public class TypesTest extends TestCase { assertEquals("Minute mismatch: ", 4, actual.get(Calendar.MINUTE)); assertEquals("Second mismatch: ", 5, actual.get(Calendar.SECOND)); } + + public void testUUID() { + Random rnd = new Random(); + byte[] bUUID = new byte[16]; + rnd.nextBytes(bUUID); + + UUID uuid = Types.bytestoUUID(bUUID); + byte[] eUUID = Types.UUIDtoBytes(uuid); + + assertArrayEquals("UUID match failed", bUUID, eUUID); + } } \ No newline at end of file diff --git a/tests/src/com/keepassdroid/tests/database/DeleteEntry.java b/tests/src/com/keepassdroid/tests/database/DeleteEntry.java index b47223779..0e97be845 100644 --- a/tests/src/com/keepassdroid/tests/database/DeleteEntry.java +++ b/tests/src/com/keepassdroid/tests/database/DeleteEntry.java @@ -25,6 +25,7 @@ import android.content.Context; import android.test.AndroidTestCase; import com.keepassdroid.Database; +import com.keepassdroid.database.PwDatabase; import com.keepassdroid.database.PwDatabaseV3; import com.keepassdroid.database.PwEntry; import com.keepassdroid.database.PwGroup; @@ -100,7 +101,7 @@ public class DeleteEntry extends AndroidTestCase { } - private PwGroup getGroup(PwDatabaseV3 pm, String name) { + private PwGroup getGroup(PwDatabase pm, String name) { Vector groups = pm.getGroups(); for ( int i = 0; i < groups.size(); i++ ) { PwGroup group = groups.get(i);