From 3d01e9e05852868d52f34610490de5a4ac11568a Mon Sep 17 00:00:00 2001
From: peli0101
Date: Thu, 15 Jan 2009 19:15:33 +0000
Subject: [PATCH] Initial import of Android Password Safe under the new name OI
Safe.
Original location: Revision 98 of android-passwordsafe at Google Code from the branch "service": http://code.google.com/p/android-passwordsafe/source/browse/?r=98#svn/branches/service
The namespace has been changed from com.bitsetters.android.passwordsafe to org.openintents.safe.
git-svn-id: http://openintents.googlecode.com/svn/trunk/Safe@1724 72b678ce-9140-0410-bee8-679b907dd61a
---
.classpath | 6 +
.project | 33 +
AUTHORS | 4 +
AndroidManifest.xml | 61 ++
CHANGELOG | 27 +
NOTES | 21 +
README | 35 +
assets/help.html | 163 +++
res/anim/cycle_7.xml | 22 +
res/anim/shake.xml | 23 +
res/drawable/passicon.png | Bin 0 -> 4696 bytes
res/layout-land/front_door.xml | 87 ++
res/layout/cat_edit.xml | 45 +
res/layout/cat_list.xml | 35 +
res/layout/chg_pass.xml | 75 ++
res/layout/front_door.xml | 81 ++
res/layout/help.xml | 34 +
res/layout/log_off.xml | 82 ++
res/layout/pass_edit.xml | 98 ++
res/layout/pass_gen.xml | 103 ++
res/layout/pass_list.xml | 35 +
res/layout/pass_row.xml | 34 +
res/layout/restore.xml | 51 +
res/values/arrays.xml | 36 +
res/values/colors.xml | 24 +
res/values/strings.xml | 129 +++
res/xml/preferences.xml | 40 +
.../openintents/intents/CryptoIntents.java | 131 +++
src/org/openintents/safe/AskPassword.java | 234 +++++
src/org/openintents/safe/Backup.java | 135 +++
src/org/openintents/safe/CSVReader.java | 251 +++++
src/org/openintents/safe/CSVWriter.java | 413 ++++++++
src/org/openintents/safe/CategoryEdit.java | 162 +++
src/org/openintents/safe/CategoryEntry.java | 26 +
src/org/openintents/safe/CategoryList.java | 974 ++++++++++++++++++
src/org/openintents/safe/ChangePass.java | 379 +++++++
src/org/openintents/safe/CryptoHelper.java | 316 ++++++
.../safe/CryptoHelperException.java | 30 +
src/org/openintents/safe/DBHelper.java | 682 ++++++++++++
src/org/openintents/safe/FrontDoor.java | 408 ++++++++
src/org/openintents/safe/Help.java | 112 ++
src/org/openintents/safe/LogOffActivity.java | 56 +
src/org/openintents/safe/PassEdit.java | 331 ++++++
src/org/openintents/safe/PassEntry.java | 45 +
src/org/openintents/safe/PassGen.java | 207 ++++
src/org/openintents/safe/PassList.java | 417 ++++++++
src/org/openintents/safe/Preferences.java | 16 +
src/org/openintents/safe/Restore.java | 292 ++++++
src/org/openintents/safe/RestoreDataSet.java | 129 +++
src/org/openintents/safe/RestoreHandler.java | 178 ++++
.../safe/service/ServiceDispatch.aidl | 25 +
.../safe/service/ServiceDispatchImpl.java | 137 +++
.../safe/service/ServiceNotification.java | 70 ++
tests/NOTES | 8 +
tests/passwordsafe-test1.csv | 78 ++
tests/passwordsafe-test2.csv | 78 ++
tests/passwordsafe-test3.csv | 349 +++++++
tests/passwordsafe-test4.csv | 78 ++
tests/passwordsafe-test5.csv | 372 +++++++
tests/passwordsafe-test6.csv | 7 +
60 files changed, 8510 insertions(+)
create mode 100644 .classpath
create mode 100644 .project
create mode 100644 AUTHORS
create mode 100644 AndroidManifest.xml
create mode 100644 CHANGELOG
create mode 100644 NOTES
create mode 100644 README
create mode 100644 assets/help.html
create mode 100644 res/anim/cycle_7.xml
create mode 100644 res/anim/shake.xml
create mode 100644 res/drawable/passicon.png
create mode 100644 res/layout-land/front_door.xml
create mode 100644 res/layout/cat_edit.xml
create mode 100644 res/layout/cat_list.xml
create mode 100644 res/layout/chg_pass.xml
create mode 100644 res/layout/front_door.xml
create mode 100644 res/layout/help.xml
create mode 100644 res/layout/log_off.xml
create mode 100644 res/layout/pass_edit.xml
create mode 100644 res/layout/pass_gen.xml
create mode 100644 res/layout/pass_list.xml
create mode 100644 res/layout/pass_row.xml
create mode 100644 res/layout/restore.xml
create mode 100644 res/values/arrays.xml
create mode 100644 res/values/colors.xml
create mode 100644 res/values/strings.xml
create mode 100644 res/xml/preferences.xml
create mode 100644 src/org/openintents/intents/CryptoIntents.java
create mode 100644 src/org/openintents/safe/AskPassword.java
create mode 100644 src/org/openintents/safe/Backup.java
create mode 100644 src/org/openintents/safe/CSVReader.java
create mode 100644 src/org/openintents/safe/CSVWriter.java
create mode 100644 src/org/openintents/safe/CategoryEdit.java
create mode 100644 src/org/openintents/safe/CategoryEntry.java
create mode 100644 src/org/openintents/safe/CategoryList.java
create mode 100644 src/org/openintents/safe/ChangePass.java
create mode 100644 src/org/openintents/safe/CryptoHelper.java
create mode 100644 src/org/openintents/safe/CryptoHelperException.java
create mode 100644 src/org/openintents/safe/DBHelper.java
create mode 100644 src/org/openintents/safe/FrontDoor.java
create mode 100644 src/org/openintents/safe/Help.java
create mode 100644 src/org/openintents/safe/LogOffActivity.java
create mode 100644 src/org/openintents/safe/PassEdit.java
create mode 100644 src/org/openintents/safe/PassEntry.java
create mode 100644 src/org/openintents/safe/PassGen.java
create mode 100644 src/org/openintents/safe/PassList.java
create mode 100644 src/org/openintents/safe/Preferences.java
create mode 100644 src/org/openintents/safe/Restore.java
create mode 100644 src/org/openintents/safe/RestoreDataSet.java
create mode 100644 src/org/openintents/safe/RestoreHandler.java
create mode 100644 src/org/openintents/safe/service/ServiceDispatch.aidl
create mode 100644 src/org/openintents/safe/service/ServiceDispatchImpl.java
create mode 100644 src/org/openintents/safe/service/ServiceNotification.java
create mode 100644 tests/NOTES
create mode 100644 tests/passwordsafe-test1.csv
create mode 100644 tests/passwordsafe-test2.csv
create mode 100644 tests/passwordsafe-test3.csv
create mode 100644 tests/passwordsafe-test4.csv
create mode 100644 tests/passwordsafe-test5.csv
create mode 100644 tests/passwordsafe-test6.csv
diff --git a/.classpath b/.classpath
new file mode 100644
index 0000000..ef7c361
--- /dev/null
+++ b/.classpath
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/.project b/.project
new file mode 100644
index 0000000..9673fe4
--- /dev/null
+++ b/.project
@@ -0,0 +1,33 @@
+
+
+ OI Safe
+
+
+
+
+
+ com.android.ide.eclipse.adt.ResourceManagerBuilder
+
+
+
+
+ com.android.ide.eclipse.adt.PreCompilerBuilder
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ com.android.ide.eclipse.adt.ApkBuilder
+
+
+
+
+
+ com.android.ide.eclipse.adt.AndroidNature
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..bb4e305
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,4 @@
+Steven Osborn - http://steven.bitsetters.com
+Randy McEoin
+Isaac Potoczny-Jones
+Peli
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..a8aef3b
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CHANGELOG b/CHANGELOG
new file mode 100644
index 0000000..89113ab
--- /dev/null
+++ b/CHANGELOG
@@ -0,0 +1,27 @@
+0.1.0 [Nov 20, 2007] - Initial release of Password Safe
+0.2.0 [Sept 4, 2008] - Updated to work with Android 0.9 thanks to Randy McEoin
+0.2.1 [Sept 5, 2008] - Fixed delete, fixed bug where new entries wouldn't show
+ up immediately.
+0.2.2 [Sept 16, 2008] - Added categories and export to CSV.
+0.2.3 [Oct 21, 2008] - Assorted bug fixes. Bumped database to version 2.
+ Initial Market Version.
+0.3.0 [Nov 2, 2008] - Improved Import/Export, Help Screen,
+ [I7] Password Generator,
+ [I10] Fixed issue where initial screen ate back button
+ [I13] Fixed issue with passwords/categories not being saved after orientation change
+ Made Delete menu option disabled on list views if there is no item selected
+ Launching browser from password edit screen copies password to clip board
+0.4.0 [Nov 15, 2008]
+ [I14] Added the ChangePass activity allowing user to change the
+ password used to unlock the app and encrypt the data
+ [I19] User is not prompted before deleting a password
+ [I20] During import a progress dialog is displayed
+0.5.0 [Nov 22, 2008]
+ [I23] Added the ability for an encrypted backup/restore
+ [I24] Offer a ContextMenu on CategoryList when user long presses
+ [I25] Fixed crashing when user hits Go on an invalid URL in PassEdit
+0.6.0 [Dec 18, 2008]
+ Changed encryption method to use a generated master key. User provides
+ a master password that decrypts the master key. The master key is used
+ to encrypt/decrypt the password entries.
+ Database is now version 3. No upgrade path provided, user must export/import.
diff --git a/NOTES b/NOTES
new file mode 100644
index 0000000..7e805d4
--- /dev/null
+++ b/NOTES
@@ -0,0 +1,21 @@
+
+To grab the database to work with it locally::
+
+adb pull /data/data/com.bitsetters.android.passwordsafe/databases/passwordsafe .
+
+
+Then to open it with sqlite3::
+
+sqlite3 passwordsafe
+
+To delete the database and force a clean start::
+
+adb shell rm /data/data/com.bitsetters.android.passwordsafe/databases/passwordsafe
+
+To uninstall the application completely::
+
+adb uninstall com.bitsetters.android.passwordsafe
+
+To make the keyword Id update::
+
+svn propset svn:keywords Id src/com/bitsetters/android/passwordsafe/filename.java
diff --git a/README b/README
new file mode 100644
index 0000000..2b91eb1
--- /dev/null
+++ b/README
@@ -0,0 +1,35 @@
+LEGAL
+------------------------------------------------------------------------------
+Key Icon is Copyright David Vignoni, http://www.icon-king.com/ and licensed
+under the GNU LGPL V2.1 http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
+
+ALL OTHER SOURCE CODE AND CONTENT:
+
+Copyright 2007, Steven Osborn - http://steven.bitsetters.com and
+Randy McEoin
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+------------------------------------------------------------------------------
+
+Features planned for version 1.0
+------------------------------------------------------------------------------
+* Store username, password, site name, notes
+* Encrypt all data using password based encryption
+* A way to launch url in browser from password safe
+
+Ideas for future versions:
+------------------------------------------------------------------------------
+* Favicon fetching and display
+* Customizeable encryption levels
+* Browser plugin
\ No newline at end of file
diff --git a/assets/help.html b/assets/help.html
new file mode 100644
index 0000000..1c4314a
--- /dev/null
+++ b/assets/help.html
@@ -0,0 +1,163 @@
+
+
+
+
+
+
Help
+
+
Categories
+
+
Categories are used to contain passwords. You might have one for Work and
+ one for Personal. This helps keep your passwords organized.
+
+
Begin by adding one via the Menu. After you've created one, you can touch it
+ to open that category. Once a category is selected you may then create password entries.
+
+
+
Passwords
+
+
After first selecting a category, you may Edit, Add or Delete password entries
+ from the Password List activity.
+
+
Password entries consist of the following fields:
+
+
+
Description
+
Description is a required field. This is what is shown in the password list.
+
+
Website
+
If appropriate, provide the full URL to the website associated with this entry.
+
+
Username
+
Username for the password, if appropriate.
+
+
Password
+
Finally the password itself.
+
+
Notes
+
Miscellaneous notes about this entry. You may type as much as you want, the field
+ will expand to accommodate.
+
+
+
Master Password
+
+
When you run Password Safe for the very first time, you are
+ prompted for a 'Master Password'. This password is
+ used to encrypt a generated Master Key. The Master Key is in turn
+ used to encrypt and decrypt all the password entries.
+
+
Each time that you launch Password Safe, the Master Password that
+ you type is used to decrypt, or unlock, the Master Key. The Master
+ Key is used to decrypt/encrypt the password entries.
+
+
The Master Key does not change. When you use the 'Change Master Password'
+ Menu option, you are merely re-encrypting the Master Key with a new
+ password. The encrypted password entries are untouched, since
+ they are encrypted with the Master Key.
+
+
Backup and Restore
+
+
You should backup your database of passwords from time to time. To do this
+ use Menu -> Backup. This will create a file named 'passwordsafe.xml' on
+ the sdcard. Connect your phone to a computer and allow the USB mounting.
+ Look for 'passwordsafe.xml' and copy that to a safe location on your computer.
+ The file is encrypted using the same encryption as the database on your phone.
+
+
Import and Export
+
+
You can import data into Password Safe using CSV files. It is easiest to
+ create and manage a CSV within a spreadsheet program like Excel or
+ OpenOffice. Create a file with the following columns in the first row.
+
+
+
+
Category
+
Description
+
Website
+
Username
+
Password
+
Notes
+
+
+
+
Password Safe import is very sensitive about the first row,
+ so please specify the first
+ row exactly like the above list. You may also find it helpful to
+ put some data into the Password Safe and perform an export first, so
+ that you can see what the CSV should look like.
+
+
Use the name 'passwordsafe.csv' for your filename. Connect the
+ phone to your computer and copy 'passwordsafe.csv' onto the phone.
+ The file must be in the base directory of the phone's USB storage.
+
+
Once the CSV file is in place, use Menu -> Import to import the file.
+ Be sure that your phone is not connected to your computer as the sdcard
+ is not allowed to be accessed by both your computer and the phone at
+ the same time.
+
+
Exporting is a snap. Just use Menu -> Export. This will create the
+ file 'passwordsafe.csv'. Connect your phone to a computer to retrieve
+ this file.
+
WARNING: The exported file is not encrypted!!
+ After exporting and working with the file, you should delete it
+ as soon as possible!
+
+
Security Concerns
+
+
Password Safe was written to safely store your passwords so that nobody can see
+ them other than you.
+ The password you use to unlock Password Safe is used to encrypt the data.
+ Data is stored within a SQLite database within your phone. That database stays
+ local. It does not get transferred to the Internet. In fact, the application
+ doesn't have the security privileges to even access the Internet.
+
+
Because Password Safe cannot access the Internet, the data is not
+ automatically backed up.
+ In order to backup, you will need to use Menu -> Backup.
+
+
Locking
+
+
Any of the following actions will cause Password Safe to lock:
+
+
+
The screen turns off. This is a default behavior of the phone and is controlled
+ in the phone's Settings -> Sound & display -> Screen timeout.
+ The default timeout is 1 minute.
+ After 1 minute the phone will go to sleep. When this happens, Password Safe will
+ lock itself.
+
You select Lock from the Menu on the Categories screen.
+
Android only allows six active applications. Once you launch the seventh,
+ the oldest used application will be killed. If Password Safe is killed, than
+ the next time you launch it, it will be locked.
+
+
+
Really, the easiest way to lock is to just hit the power button on the phone briefly and
+ turn off the screen.
+
+
+
\ No newline at end of file
diff --git a/res/anim/cycle_7.xml b/res/anim/cycle_7.xml
new file mode 100644
index 0000000..dfbfdab
--- /dev/null
+++ b/res/anim/cycle_7.xml
@@ -0,0 +1,22 @@
+
+
+
+
\ No newline at end of file
diff --git a/res/anim/shake.xml b/res/anim/shake.xml
new file mode 100644
index 0000000..c05ab40
--- /dev/null
+++ b/res/anim/shake.xml
@@ -0,0 +1,23 @@
+
+
+
+
\ No newline at end of file
diff --git a/res/drawable/passicon.png b/res/drawable/passicon.png
new file mode 100644
index 0000000000000000000000000000000000000000..bf8d7e42a18f113dbef30c6945a6dcd03e2103e9
GIT binary patch
literal 4696
zcmX9?dpy(KAOG%RE}L5kOWEX+E_!V4F*cM^LiPMgscbP+WV*SeUC1TX&*iDCh}EyB
zJjG+uMZy+x*F0SmVV0P3*%)Sa`Tq9&et(>EUgvdQpU?T6&*$@gf6h5qLW6f08mu+|
z0KhPC=eB*?k@Qc~LumWQClZIX13boek1qgJwY%%&{?N|R=eCEP+n*49E{Sr63iw7P
z9H(Lf<0&!JeN;+RN@53<005>x1GoA9mOOA=1%kxpod^KZ(t>M(kbvt;0HDV)#Q*@r
zmE06OxH5<1Yxb=bf&eO#Hu&o;Wk%*2(>SuY`^Rq-%7~;3NtP&Dw(U}BP
zqgtc39`p~(QCLK}jV5HJ1vW=C9WD@}TedJV#fj_@s&MjNYL-ZOAqE+~$=)7r;x@8WYu
zoT;pTWY+`Z6FFPyU}tEX>Q0-n>x0PKj+Sxo4B#(PIt#;$K6`R2i`
z8{-Ts2q=ddo~i_Cc$xQL0BJ&kSpzkOEv_1ROnDUbc+P=E)w_z7bJtlB=!vfpq>d0&
zK0Ah)YJDI^oeqcEHZy{6bxbWd5KwU+v|1oHQudt00`Oj@4nN9Y-{zL^S|*BzemZuY
zZm{|?osy4J=@h6?3rh**$bOE?1{3zZWAM(6_UhwgY79B@C*?`hh(_R{o^%Yw)~~ZO
z0idxYC^`4a0}pQgSG#rc9RloxbH>x@Bt(D_r^Ows=>!57uZ8B-*u3?z{ROGYypfIS
zCN&;ev!Pd0gFjz$5C;eIvr{d{6WLlX$A??BXk9=P69t1)K=g6r71
zsr2eyoA>L-Y+;Y!y)p?JyX0=3S3|<%Lg{MLXb*4Wu=nxGE~(}#FM$wLQBgMbC5(}p
z78AO#)P=R0wPLT+8Y+L9zQ5@PrbP!z0{(Ud))xnE#H*ry;`J4L>_ZT_Vl?5N9l@)x
z?NGs-j~+MCncz+3OQY1cc~^%SyyPXNQL{iotH3HU^?^M#xz2|D>HbF$2}Z*3Bc7
z4pG;_4p~~oM-p+;onYpT`LbR@ZjX%XBvK*~8#hxB@p~bA9Zw&i#y*iqPVTja9WyfD
zO0VRhu5o_u9cHt9hxB88Y%|#E2!wiy80T}O`;V~-hfLC;#5hwU6Ou~^WPcoWE1gcg
zL-{T*2!Au@^`HgRGfZ`ZmS$gRSZ>F1&avKbU2bRII}u&6@&n@pBzpW&97D6*n$7G
zNv!GA7kRfui`!cz`$0_u5Fc~j?DGG$FG~cCuOHP8KI`G(q#IP3W=Dw}7U`MT7vG7T
zU875cWw_w54_@6XU6ua6B=oRJSUgmnl#1rg&KHSfM+36jF0#?)8u09NKL~zi8l3jq
zjr9PuZs~*Wi79>8c&<26GW^32#W;rd8|=7pODAWJkTaf2-$P%!n`lczpx6pjc_Ax3C4T<_jIK15h_w5~(5^u@tvryDtApwsWM
zxIi&s`OB{c6+c*}X;kEwe?Qv6M$a6LUfwz;EORDx?B1Dzp`uK%%-FR6<5!-NK0Vmx
zI3^az)LV(!#gHRrc)zpjDjtZL0l!+|OU2ufddGIY2azuG-`8+n@z9Ng=&0n{;!H|^
z8S!(OO36T%j}T0V&3z_X(4@LtrpHQac%ZzjKxB)wYLS8Yoe*-a1{#DmCMCAC_}v#P
zIB8_>6e={GqNpAo#{b!2jWV
z^{5jU-L3chCg$5>2!C6OG5aJ4oN_UbzJNd<4LfGPe%$w&8Fy3tTRx4VtvE240>tidgbokEcd=cWF~O`1Ta6)>8SzV)Hm@Jzq9@4F7}$3=ZcBI#q=b1JT4Xr
z%k0X{6g@(@g^bP-h!4&y5p0R^1?X3}5Z%~7iK^vg-=uu1BD&A0)eQaB%8+WiOP2q{
zR)_TRZpjd@Ea(&y@A#d4c|3)K8HOlh#4h{5l(lo1@zuqLi(ZLKqFotOp@poY2On#9@P|DDC0es
zdOxZSzhG(`uCshCzNhGu@ll9*6EDYok%$9Bi4+Rb5BjZX9eIBezBUN*oGY=8Jf3|1
zb|ytpR(6oZN?f(!9NKe$nFN88fW6*;=UuZXOs7N>y*x;zVbc^
z=|dihNyiP62>3Vqr9vN+mUaFG<1`=g9TOzAx<0NW&R=?Z3{R74{FAK@kFW?;2J`M!
zgpabc^w^HI6SE*cbry-cE#2i0V_Cd!Pcew=DPhg*UbNiCT=IO@zuQE+(I+UU=U;or
zhu(!&ezb#QotR~_U}0HVWDFaXKwuf^cHGM-odbD%Gg{(SE+NAB2G}LFVvi?aD3s)6
zc7|x_D=*{`LrtIS5rUi-niH|_8G%zlrdlsj&pi9ICvZ%X)Y`MUPUw=hF9J`S(`=tr
zg_AFP!x#5FW75CpLsN(0H>N11V*g(h5+2K1XE7|oC*#>lQlzdQ|KCQj-KxJ=alhzt
z4t-`;YbG{LN?gkrMRd-8P5D!5E0t!yRkbWY%>#QkSU;L4nMM)|Y%Z
z7CyD$Szp^SsiI89vym2c|vgdu54ImUeQ
z^F3KQBj>tH6cGkD>4)@wue`4mTYXp`-|T1rx#EZ;fbcIqDM`?Y6moc`&i52=j<}yy
z3hFw1oPpX-_V3<#M9p+-XASV6zsr9Z4x&E)MN`#l)N*9}1#?sCtFpoFZV){)6s@kF
zYnDH`*r@W<`R$VVPlawTetluzvW*grWwvjETc!PWo&k~v9>mNAzHv0#GS2E>HszXY
z;BmRJFl1tMpj(dkHmi`qoC+to^HOvd=NmE{P>!d$P7_-hF-wY@1)`SJV_BQ!VZ@$G
zirJ6T62`9^&vI3{s<&gapoGy#mGF+#fs>BqCoUom_jqR;Rfod0{Qli6RPyApT&yb#
zu>Fu{K*u{WJ7mOP5cd{TFwTcYsMWp0Pf2Itq?OD3?>z=LmJA)*t)W9I5z^yD!W1Ra
zZ<^uih0ydx8;M_dk|Z#2ZfT$P
zjZ|O;JokA#&S2oHoSYOJ#Ktm$?XyHg#h)|Ks}CaFnXW~I4LD}0__A9%DOjz>OlbjI
zs8*R9Wz}TEH3;`L+0Ar?Od|glQx*&=K#bYzwtLoU=vtpK76OPHX5SyH
zY^Njiu=imc#8k?y1wOk7l|wh0dgU8w{ZkQckKJ#6@(k(liiv0lc_wvv)szB^uz~$e
zWX<INpOptIP&Uqy2V%H3&LY;%oe5^|+`P%d5F3HY4uj?~P)by(0;n1`&NhJgl9!hc9e)GP@fk
zAv&w*3bouhc1mdLLh?<}7WFXLh+GIhESUl`yW@#CT(zIAzNL&uvNgPDBU^EtQ@Y7M
zR2f1x(99Z7RaR9o3&8XO(KOOL>tIMf`dIx4Iax6+5m)iRhD`HwJWB*xTX-uVQn6zO
zcJm|=VO>hhUt=(@$`j_Zi*z4CDfnL68v9>f@YIwgDf@~D$6Y1Yz^t0ZRdFaxBVJ9H
zO$)>pLVAOjY)*X>%wIpLWWAGo(ZZmbTxE~GT>A=iqR~{D*Xn>sIfC1?5AkABhcs5Q
zp;vaFU#J-fr(sLH-n7xAbtCd(3DG-s?#HMs8^a*b!BGJ+)?Vl1o!5vhQp8%VnNtvH
z-d<)AXbeMg?c`|7gqN&$2X)+NOQh@j@cCj^$A}&7H@b?tG*@^^CoN=Lmo!Bwfhr>o;J
z*3DxL{AQZWZnSN;W8;x5H%f?aq|(c!!}#aXLjdHmEfS|Mm1-zU@M~V@*J|&}y&S{x
zC5Ev>^KdU3kpQ?f1iXW
YypNPReTpKIwITzO?MURff{}U*MT{r+L!Rnr;+NLc^)lx_er7
zvP}nEUoVH{i{>UB^BO@-66SClw#8g$b;lRU#nJ*cM%F@c;Lhg9CygH)(5D
T02J-l5D>UMc-voo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/cat_edit.xml b/res/layout/cat_edit.xml
new file mode 100644
index 0000000..bc8b7fc
--- /dev/null
+++ b/res/layout/cat_edit.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/cat_list.xml b/res/layout/cat_list.xml
new file mode 100644
index 0000000..f554e48
--- /dev/null
+++ b/res/layout/cat_list.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/chg_pass.xml b/res/layout/chg_pass.xml
new file mode 100644
index 0000000..ab0fa2c
--- /dev/null
+++ b/res/layout/chg_pass.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/front_door.xml b/res/layout/front_door.xml
new file mode 100644
index 0000000..493238b
--- /dev/null
+++ b/res/layout/front_door.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/help.xml b/res/layout/help.xml
new file mode 100644
index 0000000..4c8f47b
--- /dev/null
+++ b/res/layout/help.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/log_off.xml b/res/layout/log_off.xml
new file mode 100644
index 0000000..2028f68
--- /dev/null
+++ b/res/layout/log_off.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/pass_edit.xml b/res/layout/pass_edit.xml
new file mode 100644
index 0000000..ec1ea70
--- /dev/null
+++ b/res/layout/pass_edit.xml
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/pass_gen.xml b/res/layout/pass_gen.xml
new file mode 100644
index 0000000..97d56bd
--- /dev/null
+++ b/res/layout/pass_gen.xml
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/pass_list.xml b/res/layout/pass_list.xml
new file mode 100644
index 0000000..12baa0b
--- /dev/null
+++ b/res/layout/pass_list.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/pass_row.xml b/res/layout/pass_row.xml
new file mode 100644
index 0000000..aa09e98
--- /dev/null
+++ b/res/layout/pass_row.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/restore.xml b/res/layout/restore.xml
new file mode 100644
index 0000000..0021858
--- /dev/null
+++ b/res/layout/restore.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
new file mode 100644
index 0000000..b9a1012
--- /dev/null
+++ b/res/values/arrays.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+ 1 minute
+ 5 minutes
+ 30 minutes
+
+
+
+
+ 1
+ 5
+ 30
+
+
+
+
\ No newline at end of file
diff --git a/res/values/colors.xml b/res/values/colors.xml
new file mode 100644
index 0000000..8de9e28
--- /dev/null
+++ b/res/values/colors.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ #e5edfa
+ #ffffff
+
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100644
index 0000000..6a5d50b
--- /dev/null
+++ b/res/values/strings.xml
@@ -0,0 +1,129 @@
+
+
+
+
+ Password Safe
+ Steven Osborn, Randy McEoin
+ 0.6.0
+ No Passwords present for this category.
+ No categories present. Please add one via Menu->Add
+ Lock
+ Edit
+ Add
+ Delete
+ Password
+ Passwords
+ Master Password
+ Username
+ Website
+ Description
+ Notes
+ Save
+ Invalid password. Please try again.
+ Password must be at least 4 characters
+ Description cannot be blank
+ Name cannot be blank
+ Please enter a password below, which will be used to keep your passwords safe.
+ You are currently logged into Android Passwordsafe, meaning that other applications can request their passwords and decrypt/encrypt data. Lock to prevent further access until you type in your password again.
+ Another application is asking to use the master key. Please type in your password to unlock it.
+ Categories
+ Category
+ Business
+ Personal
+ Name
+ Category is not empty. You must delete the passwords first.
+ Cannot decrypt category
+ Cannot decrypt password
+ Error exporting database
+ Export Database
+ Export to 'passwordsafe.csv' successful. Warning! The file is not encrypted.
+ Unable to create export file 'passwordsafe.csv' on SDCARD. You might try disconnecting USB from computer.
+ Import Database
+ Import file is missing:
+ Unable to import file 'passwordsafe.csv' from SDCARD. You might try disconnecting USB from computer.
+ First line is missing fields.
+ Import
+ Do you want to replace the database?
+ Yes
+ No
+ Please wait while importing...
+ No categories found.
+ No entries found in CSV.
+ Too many categories.
+ Not enough fields
+ Blank category
+ Blank description
+ Delete Database
+ Are you really sure you want to delete the database? This cannot be undone.
+ Added
+ entries
+ Import complete
+ Since it is not secure to keep passwords in clear text CSV files, would you like to delete
+ Edit Entry
+ Help
+ Close
+ Go
+ Delete Password
+ Are you sure you want to delete this password?
+ Change Master Password
+ Use this to change the password you use to unlock Password Safe. Your data will be re-encrypted with the new password. The more complex your password is, the safer your data will be.
+ Old password
+ New password
+ Verify password
+ New and Verify passwords do not match.
+ Old password is invalid. Please try again.
+ Error changing password
+ Master Password changed
+ Confirm
+ Password entries do not match.
+ Backup
+ Backing up to
+ Complete. Passwords backed up:
+ Backup failed:
+ Restore
+ Filename
+ Backup file not found. You need to perform a backup first to create it. Or copy a backup from your PC.
+ Please provide the password used by the backup file. Your master password will be set to this.
+ Unable to open file:
+ Version not supported:
+ Restore error.
+ Bad password.
+ Restore Database
+ Are you sure you want to replace the current data?
+ Found
+ passwords from backup dated
+ Complete. Passwords restored:
+ Copying Password to Clipboard
+ Invalid website
+ Open
+ Database Version Error
+ Sorry, but the version of the database is not supported. You must install the version associated with this database.
+ access stored password
+ Allows the application to encrypt and decrypt text, and access the passwords stored by Android Passwordsafe.
+ Move
+ Select Category
+ Moved to
+ Preferences
+ Allow external access
+ Allow other applications to access safe.
+ Lock timeout
+ Minutes before timeout occurs and safe is locked.
+ Lock timeout
+
diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml
new file mode 100644
index 0000000..947d475
--- /dev/null
+++ b/res/xml/preferences.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/org/openintents/intents/CryptoIntents.java b/src/org/openintents/intents/CryptoIntents.java
new file mode 100644
index 0000000..6f5ba4b
--- /dev/null
+++ b/src/org/openintents/intents/CryptoIntents.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2008 Isaac Potoczny-Jones
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openintents.intents;
+
+/**
+ * @version Jan 11, 2008, 11:50 UTC
+ *
+ * @author Isaac Potoczny-Jones
+ * @author Peli
+ *
+ */
+public class CryptoIntents {
+
+ /**
+ * Activity Action: Encrypt all strings given in the extra(s) EXTRA_TEXT or
+ * EXTRA_TEXT_ARRAY.
+ * Returns all encrypted string in the same extra(s).
+ *
+ *
Constant Value: "org.openintents.action.ENCRYPT"
+ */
+ public static final String ACTION_ENCRYPT = "org.openintents.action.ENCRYPT";
+
+ /**
+ * Activity Action: Decrypt all strings given in the extra TEXT or
+ * EXTRA_TEXT_ARRAY.
+ * Returns all decrypted string in the same extra(s).
+ *
+ *
Constant Value: "org.openintents.action.DECRYPT"
+ */
+ public static final String ACTION_DECRYPT = "org.openintents.action.DECRYPT";
+
+ /**
+ * Activity Action: Get the password corresponding to the category of the
+ * calling application, and the EXTRA_DESCRIPTION, as provided.
+ * Returns the decrypted username & password in the extras EXTRA_USERNAME and
+ * EXTRA_PASSWORD. CATEGORY is an optional parameter.
+ *
+ *
+ */
+ public static final String ACTION_GET_PASSWORD = "org.openintents.action.GET_PASSWORD";
+
+ /**
+ * Activity Action: Set the password corresponding to the category of the
+ * calling application, and the EXTRA_DESCRIPTION, EXTRA_USERNAME and
+ * EXTRA_PASSWORD as provided. CATEGORY is an optional parameter.
+ *
+ * If both username and password are the non-null empty string, delete this
+ * password entry.
+ *
+ */
+ public static final String ACTION_SET_PASSWORD = "org.openintents.action.SET_PASSWORD";
+
+ /**
+ * Broadcast Action: Sent when the user got logged out of the
+ * crypto session.
+ *
+ * This can happen after the user logs out actively,
+ * or through a time-out.
+ *
+ * Activities that show decrypted content should hide that content again.
+ *
+ *
+ */
+ public static final String ACTION_CRYPTO_LOGGED_OUT = "org.openintents.action.CRYPTO_LOGGED_OUT";
+
+ /**
+ * The text to encrypt or decrypt, or the location for the return result.
+ *
+ *
Constant Value: "org.openintents.extra.TEXT"
+ */
+ public static final String EXTRA_TEXT = "org.openintents.extra.TEXT";
+
+ /**
+ * An array of text to encrypt or decrypt, or the location for the return result.
+ * Use this to encrypt several strings at once.
+ *
+ * Entries of the array that are null will be simply ignored and not
+ * encrypted or decrypted.
+ *
+ *
+ */
+ public static final String EXTRA_TEXT_ARRAY = "org.openintents.extra.TEXT_ARRAY";
+
+ /**
+ * Required input parameter to GET_PASSWORD and SET_PASSWORD. Corresponds to the "description"
+ * field in passwordsafe. Should be a unique name for the password you're using,
+ * and will already be specific to your application, ie "org.syntaxpolice.opensocial"
+ *
+ *
+ */
+ public static final String EXTRA_UNIQUE_NAME = "org.openintents.extra.UNIQUE_NAME";
+
+ /**
+ * Output parameter from GET_PASSWORD and optional input parameter to SET_PASSWORD.
+ * Corresponds to the decrypted "username" field in passwordsafe.
+ *
+ *
Constant Value: "org.openintents.extra.USERNAME"
+ */
+ public static final String EXTRA_USERNAME = "org.openintents.extra.USERNAME";
+
+ /**
+ * Output parameter from GET_PASSWORD and _required_ input parameter to SET_PASSWORD.
+ * Corresponds to the decrypted "password" field in passwordsafe.
+ *
+ *
Constant Value: "org.openintents.extra.PASSWORD"
+ */
+ public static final String EXTRA_PASSWORD = "org.openintents.extra.PASSWORD";
+
+ /**
+ * Whether to prompt for the password if the service is not running yet.
+ *
+ * Default value is 'true'. Set to 'false' if you want to suppress prompting for
+ * a password.
+ *
+ *
Constant Value: "org.openintents.extra.PROMPT"
+ */
+ public static final String EXTRA_PROMPT = "org.openintents.extra.PROMPT";
+}
diff --git a/src/org/openintents/safe/AskPassword.java b/src/org/openintents/safe/AskPassword.java
new file mode 100644
index 0000000..6114117
--- /dev/null
+++ b/src/org/openintents/safe/AskPassword.java
@@ -0,0 +1,234 @@
+/* $Id: AskPassword.java 95 2009-01-11 11:37:59Z peli0101 $
+ *
+ * Copyright 2007-2008 Steven Osborn
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openintents.safe;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+/**
+ * AskPassword Activity
+ *
+ * This activity just acts as a splash screen and gets the password from the
+ * user that will be used to decrypt/encrypt password entries.
+ *
+ * @author Steven Osborn - http://steven.bitsetters.com
+ */
+public class AskPassword extends Activity {
+
+ private boolean debug = false;
+ private static String TAG = "AskPassword";
+ public static String EXTRA_IS_LOCAL = "org.openintents.safe.bundle.EXTRA_IS_REMOTE";
+
+ private EditText pbeKey;
+ private DBHelper dbHelper;
+ private TextView introText;
+ private TextView confirmText;
+ private TextView remoteAsk;
+ private EditText confirmPass;
+ private String PBEKey;
+ private String masterKey;
+ private CryptoHelper ch;
+ private boolean firstTime = false;
+
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ Intent thisIntent = getIntent();
+
+ boolean isLocal = thisIntent.getBooleanExtra (EXTRA_IS_LOCAL, false);
+
+ if (debug)
+ Log.d(TAG, "onCreate()");
+
+ dbHelper = new DBHelper(this);
+
+ ch = new CryptoHelper(CryptoHelper.EncryptionStrong);
+ if (dbHelper.needsUpgrade()) {
+ switch (dbHelper.fetchVersion()) {
+ case 2:
+ databaseVersionError();
+ }
+ }
+
+ // Setup layout
+ setContentView(R.layout.front_door);
+ ImageView icon = (ImageView) findViewById(R.id.entry_icon);
+ icon.setImageResource(R.drawable.passicon);
+ TextView header = (TextView) findViewById(R.id.entry_header);
+ String version = getString(R.string.version);
+ String appName = getString(R.string.app_name);
+ String head = appName + " " + version + "\n";
+ header.setText(head);
+
+ pbeKey = (EditText) findViewById(R.id.password);
+ introText = (TextView) findViewById(R.id.first_time);
+ remoteAsk = (TextView) findViewById(R.id.remote);
+ confirmPass = (EditText) findViewById(R.id.pass_confirm);
+ confirmText = (TextView) findViewById(R.id.confirm_lbl);
+ masterKey = dbHelper.fetchMasterKey();
+ if (masterKey.length() == 0) {
+ firstTime = true;
+ introText.setVisibility(View.VISIBLE);
+ confirmText.setVisibility(View.VISIBLE);
+ confirmPass.setVisibility(View.VISIBLE);
+ }
+ if (! isLocal) {
+ if (remoteAsk != null) {
+ remoteAsk.setVisibility(View.VISIBLE);
+ }
+ }
+ Button continueButton = (Button) findViewById(R.id.continue_button);
+
+ continueButton.setOnClickListener(new View.OnClickListener() {
+
+ public void onClick(View arg0) {
+ PBEKey = pbeKey.getText().toString();
+ // For this version of CryptoHelper, we use the user-entered password.
+ // All other versions should be instantiated with the generated master
+ // password.
+ ch.setPassword(PBEKey);
+
+ // Password must be at least 4 characters
+ if (PBEKey.length() < 4) {
+ Toast.makeText(AskPassword.this, R.string.notify_blank_pass,
+ Toast.LENGTH_SHORT).show();
+ Animation shake = AnimationUtils
+ .loadAnimation(AskPassword.this, R.anim.shake);
+
+ findViewById(R.id.password).startAnimation(shake);
+ return;
+ }
+
+ // If it's the user's first time to enter a password,
+ // we have to store it in the database. We are going to
+ // store an encrypted hash of the password.
+ // Generate a master key, encrypt that with the pbekey
+ // and store the encrypted master key in database.
+ if (firstTime) {
+
+ // Make sure password and confirm fields match
+ if (pbeKey.getText().toString().compareTo(
+ confirmPass.getText().toString()) != 0) {
+ Toast.makeText(AskPassword.this,
+ R.string.confirm_pass_fail, Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+ masterKey = CryptoHelper.generateMasterKey();
+ Log.i(TAG, "Saving Password: " + masterKey);
+ try {
+ String encryptedMasterKey = ch.encrypt(masterKey);
+ dbHelper.storeMasterKey(encryptedMasterKey);
+ } catch (CryptoHelperException e) {
+ Log.e(TAG, e.toString());
+ }
+ } else if (!checkUserPassword()) {
+ // Check the user's password and display a
+ // message if it's wrong
+ Toast.makeText(AskPassword.this, R.string.invalid_password,
+ Toast.LENGTH_SHORT).show();
+ Animation shake = AnimationUtils
+ .loadAnimation(AskPassword.this, R.anim.shake);
+
+ findViewById(R.id.password).startAnimation(shake);
+ return;
+ }
+
+ Intent callbackIntent = new Intent();
+
+ // Return the master key to our caller. We no longer need the
+ // user-entered PBEKey. The master key is used for everything
+ // from here on out.
+ callbackIntent.putExtra("masterKey", masterKey);
+ setResult(RESULT_OK, callbackIntent);
+ finish();
+ }
+ });
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ if (debug)
+ Log.d(TAG, "onPause()");
+
+ dbHelper.close();
+ dbHelper = null;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onPause();
+
+ if (debug)
+ Log.d(TAG, "onResume()");
+ if (dbHelper == null) {
+ dbHelper = new DBHelper(this);
+ }
+
+ }
+
+ private void databaseVersionError() {
+ Dialog about = new AlertDialog.Builder(this)
+ .setIcon(R.drawable.passicon)
+ .setTitle(R.string.database_version_error_title)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ })
+ .setMessage(R.string.database_version_error_msg)
+ .create();
+ about.show();
+
+ }
+ /**
+ *
+ * @return
+ */
+ private boolean checkUserPassword() {
+ String encryptedMasterKey = dbHelper.fetchMasterKey();
+ String decryptedMasterKey = "";
+ try {
+ decryptedMasterKey = ch.decrypt(encryptedMasterKey);
+ } catch (CryptoHelperException e) {
+ Log.e(TAG, e.toString());
+ }
+ if (ch.getStatus()==true) {
+ masterKey=decryptedMasterKey;
+ return true;
+ }
+ masterKey=null;
+ return false;
+ }
+}
diff --git a/src/org/openintents/safe/Backup.java b/src/org/openintents/safe/Backup.java
new file mode 100644
index 0000000..f1c4b28
--- /dev/null
+++ b/src/org/openintents/safe/Backup.java
@@ -0,0 +1,135 @@
+/* $Id: Backup.java 71 2008-12-28 19:42:48Z peli0101 $
+ *
+ * Copyright 2008 Randy McEoin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openintents.safe;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.util.Date;
+import java.util.List;
+import android.content.Context;
+import android.util.Log;
+import android.util.Xml;
+
+public class Backup {
+
+ private static boolean debug = false;
+ private static final String TAG = "Backup";
+
+ public static int CURRENT_VERSION = 2;
+
+ private String result="";
+
+ Context myCtx=null;
+
+ public Backup(Context ctx) {
+ myCtx=ctx;
+ }
+
+ public boolean write(String filename) {
+ if (debug) Log.d(TAG,"write("+filename+",)");
+
+ try {
+ FileOutputStream str = new FileOutputStream(filename);
+ org.xmlpull.v1.XmlSerializer serializer = Xml.newSerializer();
+ serializer.setOutput(str, "utf-8");
+ serializer.startDocument(null, Boolean.valueOf(true));
+ serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+ serializer.startTag(null, "AndroidPasswordSafe");
+
+ serializer.attribute(null, "version", Integer.toString(CURRENT_VERSION));
+
+ Date today;
+ String dateOut;
+ DateFormat dateFormatter;
+
+ dateFormatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT,
+ DateFormat.FULL);
+ today = new Date();
+ dateOut = dateFormatter.format(today);
+
+ serializer.attribute(null, "date", dateOut);
+
+ DBHelper dbHelper=new DBHelper(myCtx);
+
+ String masterKeyEncrypted = dbHelper.fetchMasterKey();
+ serializer.startTag(null, "MasterKey");
+ serializer.text(masterKeyEncrypted);
+ serializer.endTag(null, "MasterKey");
+
+ List crows;
+ crows = dbHelper.fetchAllCategoryRows();
+
+ int totalPasswords=0;
+
+ for (CategoryEntry crow : crows) {
+
+ serializer.startTag(null, "Category");
+ serializer.attribute(null, "name", crow.name);
+
+ List rows;
+ rows = dbHelper.fetchAllRows(crow.id);
+
+ for (PassEntry row : rows) {
+ totalPasswords++;
+
+ serializer.startTag(null, "Entry");
+
+ serializer.startTag(null, "Description");
+ serializer.text(row.description);
+ serializer.endTag(null, "Description");
+
+ serializer.startTag(null, "Website");
+ serializer.text(row.website);
+ serializer.endTag(null, "Website");
+
+ serializer.startTag(null, "Username");
+ serializer.text(row.username);
+ serializer.endTag(null, "Username");
+
+ serializer.startTag(null, "Password");
+ serializer.text(row.password);
+ serializer.endTag(null, "Password");
+
+ serializer.startTag(null, "Note");
+ serializer.text(row.note);
+ serializer.endTag(null, "Note");
+
+ serializer.endTag(null, "Entry");
+ }
+ serializer.endTag(null, "Category");
+ }
+
+ serializer.endTag(null, "AndroidPasswordSafe");
+ serializer.endDocument();
+
+ dbHelper.close();
+
+ result=myCtx.getString(R.string.backup_complete)+" "+
+ Integer.toString(totalPasswords);
+ } catch (IOException e) {
+ e.printStackTrace();
+ result=myCtx.getString(R.string.backup_failed)+" "+
+ e.getLocalizedMessage();
+ return false;
+ }
+ return true;
+ }
+ public String getResult() {
+ return result;
+ }
+}
diff --git a/src/org/openintents/safe/CSVReader.java b/src/org/openintents/safe/CSVReader.java
new file mode 100644
index 0000000..5ba0f0c
--- /dev/null
+++ b/src/org/openintents/safe/CSVReader.java
@@ -0,0 +1,251 @@
+/* $Id: CSVReader.java 71 2008-12-28 19:42:48Z peli0101 $
+ */
+package org.openintents.safe;
+
+/**
+ Copyright 2005 Bytecode Pty Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+/**
+ * This was found at http://sourceforge.net/projects/opencsv/
+ */
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A very simple CSV reader released under a commercial-friendly license.
+ *
+ * @author Glen Smith
+ *
+ */
+public class CSVReader {
+
+ private BufferedReader br;
+
+ private boolean hasNext = true;
+
+ private char separator;
+
+ private char quotechar;
+
+ private int skipLines;
+
+ private boolean linesSkiped;
+
+ /** The default separator to use if none is supplied to the constructor. */
+ public static final char DEFAULT_SEPARATOR = ',';
+
+ /**
+ * The default quote character to use if none is supplied to the
+ * constructor.
+ */
+ public static final char DEFAULT_QUOTE_CHARACTER = '"';
+
+ /**
+ * The default line to start reading.
+ */
+ public static final int DEFAULT_SKIP_LINES = 0;
+
+ /**
+ * Constructs CSVReader using a comma for the separator.
+ *
+ * @param reader
+ * the reader to an underlying CSV source.
+ */
+ public CSVReader(Reader reader) {
+ this(reader, DEFAULT_SEPARATOR);
+ }
+
+ /**
+ * Constructs CSVReader with supplied separator.
+ *
+ * @param reader
+ * the reader to an underlying CSV source.
+ * @param separator
+ * the delimiter to use for separating entries.
+ */
+ public CSVReader(Reader reader, char separator) {
+ this(reader, separator, DEFAULT_QUOTE_CHARACTER);
+ }
+
+
+
+ /**
+ * Constructs CSVReader with supplied separator and quote char.
+ *
+ * @param reader
+ * the reader to an underlying CSV source.
+ * @param separator
+ * the delimiter to use for separating entries
+ * @param quotechar
+ * the character to use for quoted elements
+ */
+ public CSVReader(Reader reader, char separator, char quotechar) {
+ this(reader, separator, quotechar, DEFAULT_SKIP_LINES);
+ }
+
+ /**
+ * Constructs CSVReader with supplied separator and quote char.
+ *
+ * @param reader
+ * the reader to an underlying CSV source.
+ * @param separator
+ * the delimiter to use for separating entries
+ * @param quotechar
+ * the character to use for quoted elements
+ * @param line
+ * the line number to skip for start reading
+ */
+ public CSVReader(Reader reader, char separator, char quotechar, int line) {
+ this.br = new BufferedReader(reader);
+ this.separator = separator;
+ this.quotechar = quotechar;
+ this.skipLines = line;
+ }
+
+ /**
+ * Reads the entire file into a List with each element being a String[] of
+ * tokens.
+ *
+ * @return a List of String[], with each String[] representing a line of the
+ * file.
+ *
+ * @throws IOException
+ * if bad things happen during the read
+ */
+ public List readAll() throws IOException {
+
+ List allElements = new ArrayList();
+ while (hasNext) {
+ String[] nextLineAsTokens = readNext();
+ if (nextLineAsTokens != null)
+ allElements.add(nextLineAsTokens);
+ }
+ return allElements;
+
+ }
+
+ /**
+ * Reads the next line from the buffer and converts to a string array.
+ *
+ * @return a string array with each comma-separated element as a separate
+ * entry.
+ *
+ * @throws IOException
+ * if bad things happen during the read
+ */
+ public String[] readNext() throws IOException {
+
+ String nextLine = getNextLine();
+ return hasNext ? parseLine(nextLine) : null;
+ }
+
+ /**
+ * Reads the next line from the file.
+ *
+ * @return the next line from the file without trailing newline
+ * @throws IOException
+ * if bad things happen during the read
+ */
+ private String getNextLine() throws IOException {
+ if (!this.linesSkiped) {
+ for (int i = 0; i < skipLines; i++) {
+ br.readLine();
+ }
+ this.linesSkiped = true;
+ }
+ String nextLine = br.readLine();
+ if (nextLine == null) {
+ hasNext = false;
+ }
+ return hasNext ? nextLine : null;
+ }
+
+ /**
+ * Parses an incoming String and returns an array of elements.
+ *
+ * @param nextLine
+ * the string to parse
+ * @return the comma-tokenized list of elements, or null if nextLine is null
+ * @throws IOException if bad things happen during the read
+ */
+ private String[] parseLine(String nextLine) throws IOException {
+
+ if (nextLine == null) {
+ return null;
+ }
+
+ List tokensOnThisLine = new ArrayList();
+ StringBuffer sb = new StringBuffer();
+ boolean inQuotes = false;
+ do {
+ if (inQuotes) {
+ // continuing a quoted section, reappend newline
+ sb.append("\n");
+ nextLine = getNextLine();
+ if (nextLine == null)
+ break;
+ }
+ for (int i = 0; i < nextLine.length(); i++) {
+
+ char c = nextLine.charAt(i);
+ if (c == quotechar) {
+ // this gets complex... the quote may end a quoted block, or escape another quote.
+ // do a 1-char lookahead:
+ if( inQuotes // we are in quotes, therefore there can be escaped quotes in here.
+ && nextLine.length() > (i+1) // there is indeed another character to check.
+ && nextLine.charAt(i+1) == quotechar ){ // ..and that char. is a quote also.
+ // we have two quote chars in a row == one quote char, so consume them both and
+ // put one on the token. we do *not* exit the quoted text.
+ sb.append(nextLine.charAt(i+1));
+ i++;
+ }else{
+ inQuotes = !inQuotes;
+ // the tricky case of an embedded quote in the middle: a,bc"d"ef,g
+ if(i>2 //not on the begining of the line
+ && nextLine.charAt(i-1) != this.separator //not at the begining of an escape sequence
+ && nextLine.length()>(i+1) &&
+ nextLine.charAt(i+1) != this.separator //not at the end of an escape sequence
+ ){
+ sb.append(c);
+ }
+ }
+ } else if (c == separator && !inQuotes) {
+ tokensOnThisLine.add(sb.toString());
+ sb = new StringBuffer(); // start work on next token
+ } else {
+ sb.append(c);
+ }
+ }
+ } while (inQuotes);
+ tokensOnThisLine.add(sb.toString());
+ return (String[]) tokensOnThisLine.toArray(new String[0]);
+
+ }
+
+ /**
+ * Closes the underlying reader.
+ *
+ * @throws IOException if the close fails
+ */
+ public void close() throws IOException{
+ br.close();
+ }
+
+}
diff --git a/src/org/openintents/safe/CSVWriter.java b/src/org/openintents/safe/CSVWriter.java
new file mode 100644
index 0000000..ab90bea
--- /dev/null
+++ b/src/org/openintents/safe/CSVWriter.java
@@ -0,0 +1,413 @@
+/* $Id: CSVWriter.java 71 2008-12-28 19:42:48Z peli0101 $
+ */
+
+package org.openintents.safe;
+
+/**
+ Copyright 2005 Bytecode Pty Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+/**
+ * This was found at http://opencsv.sourceforge.net/
+ */
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.math.BigDecimal;
+import java.sql.Clob;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.sql.Types;
+import java.text.SimpleDateFormat;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * A very simple CSV writer released under a commercial-friendly license.
+ *
+ * @author Glen Smith
+ *
+ */
+public class CSVWriter {
+
+ private Writer rawWriter;
+
+ private PrintWriter pw;
+
+ private char separator;
+
+ private char quotechar;
+
+ private char escapechar;
+
+ private String lineEnd;
+
+ /** The character used for escaping quotes. */
+ public static final char DEFAULT_ESCAPE_CHARACTER = '"';
+
+ /** The default separator to use if none is supplied to the constructor. */
+ public static final char DEFAULT_SEPARATOR = ',';
+
+ /**
+ * The default quote character to use if none is supplied to the
+ * constructor.
+ */
+ public static final char DEFAULT_QUOTE_CHARACTER = '"';
+
+ /** The quote constant to use when you wish to suppress all quoting. */
+ public static final char NO_QUOTE_CHARACTER = '\u0000';
+
+ /** The escape constant to use when you wish to suppress all escaping. */
+ public static final char NO_ESCAPE_CHARACTER = '\u0000';
+
+ /** Default line terminator uses platform encoding. */
+ public static final String DEFAULT_LINE_END = "\n";
+
+ private static final SimpleDateFormat
+ TIMESTAMP_FORMATTER =
+ new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss");
+
+ private static final SimpleDateFormat
+ DATE_FORMATTER =
+ new SimpleDateFormat("dd-MMM-yyyy");
+
+ /**
+ * Constructs CSVWriter using a comma for the separator.
+ *
+ * @param writer
+ * the writer to an underlying CSV source.
+ */
+ public CSVWriter(Writer writer) {
+ this(writer, DEFAULT_SEPARATOR);
+ }
+
+ /**
+ * Constructs CSVWriter with supplied separator.
+ *
+ * @param writer
+ * the writer to an underlying CSV source.
+ * @param separator
+ * the delimiter to use for separating entries.
+ */
+ public CSVWriter(Writer writer, char separator) {
+ this(writer, separator, DEFAULT_QUOTE_CHARACTER);
+ }
+
+ /**
+ * Constructs CSVWriter with supplied separator and quote char.
+ *
+ * @param writer
+ * the writer to an underlying CSV source.
+ * @param separator
+ * the delimiter to use for separating entries
+ * @param quotechar
+ * the character to use for quoted elements
+ */
+ public CSVWriter(Writer writer, char separator, char quotechar) {
+ this(writer, separator, quotechar, DEFAULT_ESCAPE_CHARACTER);
+ }
+
+ /**
+ * Constructs CSVWriter with supplied separator and quote char.
+ *
+ * @param writer
+ * the writer to an underlying CSV source.
+ * @param separator
+ * the delimiter to use for separating entries
+ * @param quotechar
+ * the character to use for quoted elements
+ * @param escapechar
+ * the character to use for escaping quotechars or escapechars
+ */
+ public CSVWriter(Writer writer, char separator, char quotechar, char escapechar) {
+ this(writer, separator, quotechar, escapechar, DEFAULT_LINE_END);
+ }
+
+
+ /**
+ * Constructs CSVWriter with supplied separator and quote char.
+ *
+ * @param writer
+ * the writer to an underlying CSV source.
+ * @param separator
+ * the delimiter to use for separating entries
+ * @param quotechar
+ * the character to use for quoted elements
+ * @param lineEnd
+ * the line feed terminator to use
+ */
+ public CSVWriter(Writer writer, char separator, char quotechar, String lineEnd) {
+ this(writer, separator, quotechar, DEFAULT_ESCAPE_CHARACTER, lineEnd);
+ }
+
+
+
+ /**
+ * Constructs CSVWriter with supplied separator, quote char, escape char and line ending.
+ *
+ * @param writer
+ * the writer to an underlying CSV source.
+ * @param separator
+ * the delimiter to use for separating entries
+ * @param quotechar
+ * the character to use for quoted elements
+ * @param escapechar
+ * the character to use for escaping quotechars or escapechars
+ * @param lineEnd
+ * the line feed terminator to use
+ */
+ public CSVWriter(Writer writer, char separator, char quotechar, char escapechar, String lineEnd) {
+ this.rawWriter = writer;
+ this.pw = new PrintWriter(writer);
+ this.separator = separator;
+ this.quotechar = quotechar;
+ this.escapechar = escapechar;
+ this.lineEnd = lineEnd;
+ }
+
+ /**
+ * Writes the entire list to a CSV file. The list is assumed to be a
+ * String[]
+ *
+ * @param allLines
+ * a List of String[], with each String[] representing a line of
+ * the file.
+ */
+ public void writeAll(List> allLines) {
+
+ for (Iterator> iter = allLines.iterator(); iter.hasNext();) {
+ String[] nextLine = (String[]) iter.next();
+ writeNext(nextLine);
+ }
+
+ }
+
+ protected void writeColumnNames(ResultSetMetaData metadata)
+ throws SQLException {
+
+ int columnCount = metadata.getColumnCount();
+
+ String[] nextLine = new String[columnCount];
+ for (int i = 0; i < columnCount; i++) {
+ nextLine[i] = metadata.getColumnName(i + 1);
+ }
+ writeNext(nextLine);
+ }
+
+ /**
+ * Writes the entire ResultSet to a CSV file.
+ *
+ * The caller is responsible for closing the ResultSet.
+ *
+ * @param rs the recordset to write
+ * @param includeColumnNames true if you want column names in the output, false otherwise
+ *
+ */
+ public void writeAll(java.sql.ResultSet rs, boolean includeColumnNames) throws SQLException, IOException {
+
+ ResultSetMetaData metadata = rs.getMetaData();
+
+
+ if (includeColumnNames) {
+ writeColumnNames(metadata);
+ }
+
+ int columnCount = metadata.getColumnCount();
+
+ while (rs.next())
+ {
+ String[] nextLine = new String[columnCount];
+
+ for (int i = 0; i < columnCount; i++) {
+ nextLine[i] = getColumnValue(rs, metadata.getColumnType(i + 1), i + 1);
+ }
+
+ writeNext(nextLine);
+ }
+ }
+
+ private static String getColumnValue(ResultSet rs, int colType, int colIndex)
+ throws SQLException, IOException {
+
+ String value = "";
+
+ switch (colType)
+ {
+ case Types.BIT:
+ Object bit = rs.getObject(colIndex);
+ if (bit != null) {
+ value = String.valueOf(bit);
+ }
+ break;
+ case Types.BOOLEAN:
+ boolean b = rs.getBoolean(colIndex);
+ if (!rs.wasNull()) {
+ value = Boolean.valueOf(b).toString();
+ }
+ break;
+ case Types.CLOB:
+ Clob c = rs.getClob(colIndex);
+ if (c != null) {
+ value = read(c);
+ }
+ break;
+ case Types.BIGINT:
+ case Types.DECIMAL:
+ case Types.DOUBLE:
+ case Types.FLOAT:
+ case Types.REAL:
+ case Types.NUMERIC:
+ BigDecimal bd = rs.getBigDecimal(colIndex);
+ if (bd != null) {
+ value = "" + bd.doubleValue();
+ }
+ break;
+ case Types.INTEGER:
+ case Types.TINYINT:
+ case Types.SMALLINT:
+ int intValue = rs.getInt(colIndex);
+ if (!rs.wasNull()) {
+ value = "" + intValue;
+ }
+ break;
+ case Types.JAVA_OBJECT:
+ Object obj = rs.getObject(colIndex);
+ if (obj != null) {
+ value = String.valueOf(obj);
+ }
+ break;
+ case Types.DATE:
+ java.sql.Date date = rs.getDate(colIndex);
+ if (date != null) {
+ value = DATE_FORMATTER.format(date);;
+ }
+ break;
+ case Types.TIME:
+ Time t = rs.getTime(colIndex);
+ if (t != null) {
+ value = t.toString();
+ }
+ break;
+ case Types.TIMESTAMP:
+ Timestamp tstamp = rs.getTimestamp(colIndex);
+ if (tstamp != null) {
+ value = TIMESTAMP_FORMATTER.format(tstamp);
+ }
+ break;
+ case Types.LONGVARCHAR:
+ case Types.VARCHAR:
+ case Types.CHAR:
+ value = rs.getString(colIndex);
+ break;
+ default:
+ value = "";
+ }
+
+
+ if (value == null)
+ {
+ value = "";
+ }
+
+ return value;
+
+ }
+
+ private static String read(Clob c) throws SQLException, IOException
+ {
+ StringBuffer sb = new StringBuffer( (int) c.length());
+ Reader r = c.getCharacterStream();
+ char[] cbuf = new char[2048];
+ int n = 0;
+ while ((n = r.read(cbuf, 0, cbuf.length)) != -1) {
+ if (n > 0) {
+ sb.append(cbuf, 0, n);
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Writes the next line to the file.
+ *
+ * @param nextLine
+ * a string array with each comma-separated element as a separate
+ * entry.
+ */
+ public void writeNext(String[] nextLine) {
+
+ if (nextLine == null)
+ return;
+
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < nextLine.length; i++) {
+
+ if (i != 0) {
+ sb.append(separator);
+ }
+
+ String nextElement = nextLine[i];
+ if (nextElement == null)
+ continue;
+ if (quotechar != NO_QUOTE_CHARACTER)
+ sb.append(quotechar);
+ for (int j = 0; j < nextElement.length(); j++) {
+ char nextChar = nextElement.charAt(j);
+ if (escapechar != NO_ESCAPE_CHARACTER && nextChar == quotechar) {
+ sb.append(escapechar).append(nextChar);
+ } else if (escapechar != NO_ESCAPE_CHARACTER && nextChar == escapechar) {
+ sb.append(escapechar).append(nextChar);
+ } else {
+ sb.append(nextChar);
+ }
+ }
+ if (quotechar != NO_QUOTE_CHARACTER)
+ sb.append(quotechar);
+ }
+
+ sb.append(lineEnd);
+ pw.write(sb.toString());
+
+ }
+
+ /**
+ * Flush underlying stream to writer.
+ *
+ * @throws IOException if bad things happen
+ */
+ public void flush() throws IOException {
+
+ pw.flush();
+
+ }
+
+ /**
+ * Close the underlying stream writer flushing any buffered content.
+ *
+ * @throws IOException if bad things happen
+ *
+ */
+ public void close() throws IOException {
+ pw.flush();
+ pw.close();
+ rawWriter.close();
+ }
+
+}
diff --git a/src/org/openintents/safe/CategoryEdit.java b/src/org/openintents/safe/CategoryEdit.java
new file mode 100644
index 0000000..8ea8590
--- /dev/null
+++ b/src/org/openintents/safe/CategoryEdit.java
@@ -0,0 +1,162 @@
+/* $Id: CategoryEdit.java 71 2008-12-28 19:42:48Z peli0101 $
+ *
+ * Copyright 2008 Randy McEoin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openintents.safe;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+/**
+ * CategoryEdit Activity
+ *
+ * @author Randy McEoin
+ */
+public class CategoryEdit extends Activity {
+
+ private EditText nameText;
+ private Long RowId;
+ private DBHelper dbHelper=null;
+ private CryptoHelper ch;
+
+ private static String TAG = "CategoryEdit";
+
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ Log.d(TAG, "onCreate");
+
+ ch = new CryptoHelper();
+ ch.setPassword(PassList.getMasterKey());
+
+ if (dbHelper == null){
+ dbHelper = new DBHelper(this);
+ }
+
+ String title = getResources().getString(R.string.app_name) + " - " +
+ getResources().getString(R.string.edit_entry);
+ setTitle(title);
+
+ setContentView(R.layout.cat_edit);
+
+ nameText = (EditText) findViewById(R.id.name);
+
+ Button confirmButton = (Button) findViewById(R.id.save_category);
+
+ RowId = icicle != null ? icicle.getLong(CategoryList.KEY_ID) : null;
+ if (RowId == null) {
+ Bundle extras = getIntent().getExtras();
+ RowId = extras != null ? extras.getLong(CategoryList.KEY_ID) : null;
+ }
+
+ populateFields();
+
+ confirmButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View arg0) {
+ // Don't allow the user to enter a blank name, we need
+ // something useful to show in the list
+ if(nameText.getText().toString().trim().length() == 0) {
+ Toast.makeText(CategoryEdit.this, R.string.notify_blank_name,
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ saveState();
+ setResult(RESULT_OK);
+ finish();
+ }
+ });
+ }
+
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (RowId != null) {
+ outState.putLong(CategoryList.KEY_ID, RowId);
+ } else {
+ outState.putLong(CategoryList.KEY_ID, -1);
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ Log.d(TAG, "onPause");
+ dbHelper.close();
+ dbHelper = null;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Log.d(TAG, "onResume");
+ if (dbHelper == null) {
+ dbHelper = new DBHelper(this);
+ }
+ if (!CategoryList.isSignedIn()) {
+ Intent frontdoor = new Intent(this, FrontDoor.class);
+ startActivity(frontdoor);
+ finish();
+ }
+ populateFields();
+ }
+
+ private void saveState() {
+ Log.d(TAG, "saveState");
+ CategoryEntry entry = new CategoryEntry();
+
+ String namePlain = nameText.getText().toString();
+ Log.d(TAG, "name: " + namePlain);
+
+ try {
+ entry.name = ch.encrypt(namePlain);
+ } catch(CryptoHelperException e) {
+ Log.e(TAG,e.toString());
+ }
+
+
+ if(RowId == null || RowId == -1) {
+ Log.d(TAG, "addCategory");
+ dbHelper.addCategory(entry);
+ } else {
+ Log.d(TAG, "updateCategory");
+ Log.d(TAG, "RowId: " + String.valueOf(RowId));
+ dbHelper.updateCategory(RowId, entry);
+ }
+ }
+
+ /**
+ *
+ */
+ private void populateFields() {
+ Log.d(TAG, "populateFields");
+ if (RowId != null) {
+ CategoryEntry row = dbHelper.fetchCategory(RowId);
+ if (row.id > -1) {
+ String cryptName = row.name;
+ try {
+ nameText.setText(ch.decrypt(cryptName));
+ } catch (CryptoHelperException e) {
+ Log.e(TAG,e.toString());
+ }
+ }
+ }
+ }
+}
diff --git a/src/org/openintents/safe/CategoryEntry.java b/src/org/openintents/safe/CategoryEntry.java
new file mode 100644
index 0000000..70c3831
--- /dev/null
+++ b/src/org/openintents/safe/CategoryEntry.java
@@ -0,0 +1,26 @@
+/* $Id: CategoryEntry.java 71 2008-12-28 19:42:48Z peli0101 $
+ *
+ * Copyright 2008 Randy McEoin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openintents.safe;
+
+/**
+ * @author Randy McEoin
+ */
+public class CategoryEntry extends Object {
+ public long id;
+ public String name;
+ public String plainName;
+}
diff --git a/src/org/openintents/safe/CategoryList.java b/src/org/openintents/safe/CategoryList.java
new file mode 100644
index 0000000..cb0eeca
--- /dev/null
+++ b/src/org/openintents/safe/CategoryList.java
@@ -0,0 +1,974 @@
+/* $Id: CategoryList.java 98 2009-01-15 04:33:59Z rmceoin $
+ *
+ * Copyright 2008 Randy McEoin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openintents.safe;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import org.openintents.safe.service.ServiceDispatchImpl;
+
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ListActivity;
+import android.app.ProgressDialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.View;
+import android.view.MenuItem;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.Toast;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+
+/**
+ * CategoryList Activity
+ *
+ * @author Randy McEoin
+ * @author Steven Osborn - http://steven.bitsetters.com
+ */
+public class CategoryList extends ListActivity {
+
+ private static boolean debug = false;
+ private static final String TAG = "CategoryList";
+
+ // Menu Item order
+ public static final int LOCK_CATEGORY_INDEX = Menu.FIRST;
+ public static final int OPEN_CATEGORY_INDEX = Menu.FIRST + 1;
+ public static final int EDIT_CATEGORY_INDEX = Menu.FIRST + 2;
+ public static final int ADD_CATEGORY_INDEX = Menu.FIRST + 3;
+ public static final int DEL_CATEGORY_INDEX = Menu.FIRST + 4;
+ public static final int HELP_INDEX = Menu.FIRST + 5;
+ public static final int EXPORT_INDEX = Menu.FIRST + 6;
+ public static final int IMPORT_INDEX = Menu.FIRST + 7;
+ public static final int CHANGE_PASS_INDEX = Menu.FIRST + 8;
+ public static final int BACKUP_INDEX = Menu.FIRST + 9;
+ public static final int RESTORE_INDEX = Menu.FIRST + 10;
+ public static final int PREFERENCES_INDEX = Menu.FIRST + 11;
+
+ public static final int REQUEST_ONCREATE = 0;
+ public static final int REQUEST_EDIT_CATEGORY = 1;
+ public static final int REQUEST_ADD_CATEGORY = 2;
+ public static final int REQUEST_OPEN_CATEGORY = 3;
+ public static final int REQUEST_RESTORE = 4;
+
+ protected static final int MSG_IMPORT = 0x101;
+ protected static final int MSG_FILLDATA = MSG_IMPORT + 1;
+ protected static final int MSG_BACKUP = MSG_FILLDATA + 1;
+
+ private static final int IMPORT_PROGRESS_KEY = 0;
+ private static final int BACKUP_PROGRESS_KEY = IMPORT_PROGRESS_KEY + 1;
+
+ public static final int MAX_CATEGORIES = 256;
+
+ private static final String EXPORT_FILENAME = "/sdcard/passwordsafe.csv";
+ public static final String BACKUP_FILENAME = "/sdcard/passwordsafe.xml";
+
+ public static final String KEY_ID = "id"; // Intent keys
+
+ private CryptoHelper ch=null;
+ private DBHelper dbHelper=null;
+
+ private String importMessage="";
+ private int importedEntries=0;
+ private Thread importThread=null;
+ private boolean importDeletedDatabase=false;
+
+ private Thread backupThread=null;
+
+ private static String masterKey;
+
+ private List rows;
+
+ BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
+ if (debug) Log.d(TAG,"caught ACTION_SCREEN_OFF");
+ masterKey=null;
+ }
+ }
+ };
+
+ public Handler myViewUpdateHandler = new Handler(){
+ // @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case CategoryList.MSG_BACKUP:
+ Bundle b=msg.getData();
+ String result=b.getString("msg");
+ Toast.makeText(CategoryList.this, result,
+ Toast.LENGTH_LONG).show();
+ break;
+ case CategoryList.MSG_IMPORT:
+ if (importMessage != "") {
+ Toast.makeText(CategoryList.this, importMessage,
+ Toast.LENGTH_LONG).show();
+ }
+ String deleteMsg=getString(R.string.import_delete_csv) +
+ " " + EXPORT_FILENAME + "?";
+ Dialog about = new AlertDialog.Builder(CategoryList.this)
+ .setIcon(R.drawable.passicon)
+ .setTitle(R.string.import_complete)
+ .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ File csvFile=new File(EXPORT_FILENAME);
+ csvFile.delete();
+ }
+ })
+ .setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ }
+ })
+ .setMessage(deleteMsg)
+ .create();
+ about.show();
+
+ if ((importedEntries!=0) || (importDeletedDatabase))
+ {
+ fillData();
+ }
+ break;
+ case CategoryList.MSG_FILLDATA:
+ fillData();
+ break;
+ }
+ super.handleMessage(msg);
+ }
+ };
+ /**
+ * Called when the activity is first created.
+ */
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ if (debug) Log.d(TAG,"onCreate()");
+
+ if (!isSignedIn()) {
+ Intent frontdoor = new Intent(this, FrontDoor.class);
+ startActivity(frontdoor);
+ finish();
+ }
+
+ setContentView(R.layout.cat_list);
+ String title = getResources().getString(R.string.app_name) + " - " +
+ getResources().getString(R.string.categories);
+ setTitle(title);
+
+ if (dbHelper==null) {
+ dbHelper = new DBHelper(this);
+ if (dbHelper.getPrePopulate()==true)
+ {
+ prePopulate();
+ dbHelper.clearPrePopulate();
+ }
+ }
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_SCREEN_OFF);
+ registerReceiver(mIntentReceiver, filter);
+
+ fillData();
+
+ final ListView list = getListView();
+ list.setFocusable(true);
+ list.setOnCreateContextMenuListener(this);
+ registerForContextMenu(list);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ if (debug) Log.d(TAG,"onResume()");
+ if (dbHelper == null) {
+ dbHelper = new DBHelper(this);
+ }
+
+ if (!isSignedIn()) {
+ Intent frontdoor = new Intent(this, FrontDoor.class);
+ startActivity(frontdoor);
+ finish();
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ if (debug) Log.d(TAG,"onPause()");
+
+ if ((importThread != null) && (importThread.isAlive())) {
+ if (debug) Log.d(TAG,"wait for thread");
+ int maxWaitToDie=500000;
+ try { importThread.join(maxWaitToDie); }
+ catch(InterruptedException e){} // ignore
+ }
+ if ((backupThread != null) && (backupThread.isAlive())) {
+ if (debug) Log.d(TAG,"wait for backup thread");
+ int maxWaitToDie=500000;
+ try { backupThread.join(maxWaitToDie); }
+ catch(InterruptedException e){} // ignore
+ }
+ dbHelper.close();
+ dbHelper = null;
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+
+ if (debug) Log.d(TAG,"onStop()");
+// dbHelper.close();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ unregisterReceiver(mIntentReceiver);
+ if (debug) Log.d(TAG,"onDestroy()");
+ }
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view,
+ ContextMenuInfo menuInfo) {
+
+ AdapterView.AdapterContextMenuInfo info;
+ info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+
+ menu.setHeaderTitle(rows.get(info.position).plainName);
+ menu.add(0,OPEN_CATEGORY_INDEX, 0, R.string.open)
+ .setIcon(android.R.drawable.ic_menu_view)
+ .setAlphabeticShortcut('o');
+ menu.add(0,EDIT_CATEGORY_INDEX, 0, R.string.password_edit)
+ .setIcon(android.R.drawable.ic_menu_edit)
+ .setAlphabeticShortcut('e');
+ menu.add(0, DEL_CATEGORY_INDEX, 0, R.string.password_delete)
+ .setIcon(android.R.drawable.ic_menu_delete)
+ .setAlphabeticShortcut('d');
+ }
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ onOptionsItemSelected(item);
+ return true;
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case IMPORT_PROGRESS_KEY: {
+ ProgressDialog dialog = new ProgressDialog(this);
+ dialog.setMessage(getString(R.string.import_progress));
+ dialog.setIndeterminate(false);
+ dialog.setCancelable(false);
+ return dialog;
+ }
+ case BACKUP_PROGRESS_KEY: {
+ ProgressDialog dialog = new ProgressDialog(this);
+ dialog.setMessage(getString(R.string.backup_progress)+
+ " "+BACKUP_FILENAME);
+ dialog.setIndeterminate(false);
+ dialog.setCancelable(false);
+ return dialog;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the current status of signedIn.
+ *
+ * @return True if signed in
+ */
+ public static boolean isSignedIn() {
+ if (masterKey != null) {
+ return true;
+ }
+ return false;
+ }
+
+
+ /**
+ * Sets signedIn status to false.
+ *
+ * @see org.openintents.safe.CategoryList#isSignedIn
+ */
+ public static void setSignedOut() {
+ if (debug) Log.d(TAG,"setSignedOut()");
+ masterKey=null;
+ }
+ /**
+ * Populates the category ListView
+ */
+ private void fillData() {
+ if (debug) Log.d(TAG,"fillData()");
+ // initialize crypto so that we can display readable descriptions in
+ // the list view
+ ch = new CryptoHelper();
+ if(masterKey == null) {
+ masterKey = "";
+ }
+ ch.setPassword(masterKey);
+
+ List items = new ArrayList();
+ if (dbHelper==null) {
+ return;
+ }
+ rows = dbHelper.fetchAllCategoryRows();
+
+ for (CategoryEntry row : rows) {
+ String cryptDesc = row.name;
+ row.plainName = "";
+ try {
+ row.plainName = ch.decrypt(cryptDesc);
+ } catch (CryptoHelperException e) {
+ Log.e(TAG,e.toString());
+ }
+ }
+ Collections.sort(rows, new Comparator() {
+ public int compare(CategoryEntry o1, CategoryEntry o2) {
+ return o1.plainName.compareToIgnoreCase(o2.plainName);
+ }});
+ for (CategoryEntry row : rows) {
+ items.add(row.plainName);
+ }
+
+ ArrayAdapter entries =
+ new ArrayAdapter(this, android.R.layout.simple_list_item_1, items);
+ setListAdapter(entries);
+
+ }
+
+ @Override
+ public boolean onMenuOpened(int featureId, Menu menu) {
+ MenuItem miDelete = menu.findItem(DEL_CATEGORY_INDEX);
+ MenuItem miEdit = menu.findItem(EDIT_CATEGORY_INDEX);
+ if (getSelectedItemPosition() > -1) {
+ miDelete.setEnabled(true);
+ miEdit.setEnabled(true);
+ } else {
+ miDelete.setEnabled(false);
+ miEdit.setEnabled(false);
+ }
+ return super.onMenuOpened(featureId, menu);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ menu.add(0,LOCK_CATEGORY_INDEX, 0, R.string.password_lock)
+ .setIcon(android.R.drawable.ic_lock_lock)
+ .setShortcut('0', 'l');
+ menu.add(0,EDIT_CATEGORY_INDEX, 0, R.string.password_edit)
+ .setIcon(android.R.drawable.ic_menu_edit)
+ .setShortcut('1', 'e');
+ menu.add(0,ADD_CATEGORY_INDEX, 0, R.string.password_add)
+ .setIcon(android.R.drawable.ic_menu_add)
+ .setShortcut('2', 'a');
+
+ menu.add(0, DEL_CATEGORY_INDEX, 0, R.string.password_delete)
+ .setIcon(android.R.drawable.ic_menu_delete)
+ .setShortcut('3', 'd')
+ .setEnabled(false);
+
+ menu.add(0, HELP_INDEX, 0, R.string.help)
+ .setIcon(android.R.drawable.ic_menu_help);
+
+ menu.add(0, EXPORT_INDEX, 0, R.string.export_database)
+ .setIcon(android.R.drawable.ic_menu_upload);
+ menu.add(0, IMPORT_INDEX, 0, R.string.import_database)
+ .setIcon(android.R.drawable.ic_input_get);
+
+ menu.add(0, CHANGE_PASS_INDEX, 0, R.string.change_password)
+ .setIcon(android.R.drawable.ic_menu_manage);
+
+ menu.add(0, BACKUP_INDEX, 0, R.string.backup);
+ menu.add(0, RESTORE_INDEX, 0, R.string.restore);
+
+ menu.add(0, PREFERENCES_INDEX, 0, R.string.preferences);
+
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ static void setMasterKey(String key) {
+ masterKey = key;
+ }
+
+ static String getMasterKey() {
+ return masterKey;
+ }
+
+ private void addCategory() {
+ Intent i = new Intent(this, CategoryEdit.class);
+ startActivityForResult(i,REQUEST_ADD_CATEGORY);
+ }
+
+ private void delCategory(long Id) {
+ if (dbHelper.countPasswords(Id)>0) {
+ Toast.makeText(CategoryList.this, R.string.category_not_empty,
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ dbHelper.deleteCategory(Id);
+ fillData();
+ }
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
+ int position=-1;
+ if (info==null) {
+ position=getSelectedItemPosition();
+ } else {
+ // used when this is called from a ContextMenu
+ position=info.position;
+ }
+ switch(item.getItemId()) {
+ case LOCK_CATEGORY_INDEX:
+ Intent serviceIntent = new Intent();
+ serviceIntent.setClass(this, ServiceDispatchImpl.class );
+ stopService(serviceIntent);
+ masterKey=null;
+ Intent frontdoor = new Intent(this, FrontDoor.class);
+ frontdoor.setAction(Intent.ACTION_MAIN);
+ startActivity(frontdoor);
+ finish();
+ break;
+ case OPEN_CATEGORY_INDEX:
+ launchPassList(rows.get(info.position).id);
+ break;
+ case EDIT_CATEGORY_INDEX:
+ Intent i = new Intent(this, CategoryEdit.class);
+ if (position > -1) {
+ i.putExtra(KEY_ID, rows.get(position).id);
+ startActivityForResult(i,REQUEST_EDIT_CATEGORY);
+ }
+ break;
+ case ADD_CATEGORY_INDEX:
+ addCategory();
+ break;
+ case DEL_CATEGORY_INDEX:
+ try {
+ if (position > -1) {
+ delCategory(rows.get(position).id);
+ }
+ } catch (IndexOutOfBoundsException e) {
+ // This should only happen when there are no
+ // entries to delete.
+ Log.w(TAG,e.toString());
+ }
+ break;
+ case HELP_INDEX:
+ Intent help = new Intent(this, Help.class);
+ startActivity(help);
+ break;
+ case EXPORT_INDEX:
+ exportDatabase();
+ break;
+ case IMPORT_INDEX:
+ importDatabase();
+ break;
+ case CHANGE_PASS_INDEX:
+ Intent changePass = new Intent(this, ChangePass.class);
+ startActivity(changePass);
+ break;
+ case BACKUP_INDEX:
+ backupThreadStart();
+ break;
+ case RESTORE_INDEX:
+ restoreDatabase();
+ break;
+ case PREFERENCES_INDEX:
+ Intent preferences = new Intent(this, Preferences.class);
+ startActivity(preferences);
+ break;
+ default:
+ Log.e(TAG,"Unknown itemId");
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void launchPassList(long id) {
+ Intent passList = new Intent(this, PassList.class);
+ passList.putExtra(KEY_ID, id);
+ startActivityForResult(passList,REQUEST_OPEN_CATEGORY);
+ }
+
+ private String backupDatabase() {
+ Backup backup=new Backup(this);
+
+ backup.write(BACKUP_FILENAME);
+ return backup.getResult();
+ }
+
+ /**
+ * Start a separate thread to backup the database. By running
+ * the backup in a thread it allows the main UI thread to return
+ * and permit the updating of the progress dialog.
+ */
+ private void backupThreadStart(){
+ showDialog(BACKUP_PROGRESS_KEY);
+ backupThread = new Thread(new Runnable() {
+ public void run() {
+ String result=backupDatabase();
+ dismissDialog(BACKUP_PROGRESS_KEY);
+
+ Message m = new Message();
+ m.what = CategoryList.MSG_BACKUP;
+ Bundle b = new Bundle();
+ b.putString("msg", result);
+ m.setData(b);
+ CategoryList.this.myViewUpdateHandler.sendMessage(m);
+
+ if (debug) Log.d(TAG,"thread end");
+ }
+ });
+ backupThread.start();
+ }
+
+ private void restoreDatabase() {
+// Restore restore=new Restore(myViewUpdateHandler, this);
+
+// restore.read(BACKUP_FILENAME, masterKey);
+ Intent i = new Intent(this, Restore.class);
+ startActivityForResult(i,REQUEST_RESTORE);
+ }
+
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ super.onListItemClick(l, v, position, id);
+
+ launchPassList(rows.get(position).id);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent i) {
+ super.onActivityResult(requestCode, resultCode, i);
+
+ if (dbHelper == null) {
+ dbHelper = new DBHelper(this);
+ }
+
+ if (resultCode == RESULT_OK) {
+ fillData();
+ }
+ }
+
+ private void prePopulate() {
+ addCategory(getString(R.string.category_business));
+ addCategory(getString(R.string.category_personal));
+ }
+
+ private long addCategory(String name) {
+ if (debug) Log.d(TAG,"addCategory("+name+")");
+ if ((name==null) || (name=="")) return -1;
+ CategoryEntry entry = new CategoryEntry();
+
+ String namePlain = name;
+
+ try {
+ ch = new CryptoHelper();
+ if(masterKey == null) {
+ masterKey = "";
+ }
+ ch.setPassword(masterKey);
+
+ entry.name = ch.encrypt(namePlain);
+ } catch(CryptoHelperException e) {
+ Log.e(TAG,e.toString());
+ }
+ return dbHelper.addCategory(entry);
+ }
+
+ public boolean exportDatabase(){
+ try {
+ CSVWriter writer = new CSVWriter(new FileWriter(EXPORT_FILENAME), ',');
+
+ String[] header = { getString(R.string.category),
+ getString(R.string.description),
+ getString(R.string.website),
+ getString(R.string.username),
+ getString(R.string.password),
+ getString(R.string.notes)
+ };
+ writer.writeNext(header);
+
+ ch = new CryptoHelper();
+ if(masterKey == null) {
+ masterKey = "";
+ }
+ ch.setPassword(masterKey);
+
+ HashMap categories = new HashMap();
+
+ List crows;
+ crows = dbHelper.fetchAllCategoryRows();
+
+ for (CategoryEntry row : crows) {
+ String cryptDesc = row.name;
+ row.plainName = "";
+ try {
+ row.plainName = ch.decrypt(cryptDesc);
+ categories.put(row.id, row.plainName);
+ } catch (CryptoHelperException e) {
+ Log.e(TAG,e.toString());
+ Toast.makeText(CategoryList.this, R.string.cannot_decrypt_category,
+ Toast.LENGTH_SHORT).show();
+ return false;
+ }
+ }
+
+ List rows;
+ rows = dbHelper.fetchAllRows(new Long(0));
+
+ for (PassEntry row : rows) {
+ String cryptDesc = row.description;
+ String cryptWebsite = row.website;
+ String cryptUsername = row.username;
+ String cryptPassword = row.password;
+ String cryptNote = row.note;
+ row.plainDescription = "";
+ row.plainWebsite = "";
+ row.plainUsername = "";
+ row.plainPassword = "";
+ row.plainNote = "";
+ try {
+ row.plainDescription = ch.decrypt(cryptDesc);
+ row.plainWebsite = ch.decrypt(cryptWebsite);
+ row.plainUsername = ch.decrypt(cryptUsername);
+ row.plainPassword = ch.decrypt(cryptPassword);
+ row.plainNote = ch.decrypt(cryptNote);
+ } catch (CryptoHelperException e) {
+ Log.e(TAG,e.toString());
+ Toast.makeText(CategoryList.this, R.string.cannot_decrypt_password,
+ Toast.LENGTH_SHORT).show();
+ return false;
+ }
+ String[] rowEntries = { categories.get(row.category),
+ row.plainDescription,
+ row.plainWebsite,
+ row.plainUsername,
+ row.plainPassword,
+ row.plainNote
+ };
+ writer.writeNext(rowEntries);
+ }
+ writer.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ Toast.makeText(CategoryList.this, R.string.export_file_error,
+ Toast.LENGTH_SHORT).show();
+ return false;
+ }
+ Toast.makeText(CategoryList.this, R.string.export_success,
+ Toast.LENGTH_LONG).show();
+ return true;
+ }
+
+ private void deleteDatabaseNow(){
+ dbHelper.deleteDatabase();
+ }
+
+ public void deleteDatabase4Import(){
+// Log.i(TAG,"deleteDatabase4Import");
+ Dialog about = new AlertDialog.Builder(this)
+ .setIcon(R.drawable.passicon)
+ .setTitle(R.string.dialog_delete_database_title)
+ .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ deleteDatabaseNow();
+ importDeletedDatabase=true;
+ importDatabaseThreadStart();
+ }
+ })
+ .setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ }
+ })
+ .setMessage(R.string.dialog_delete_database_msg)
+ .create();
+ about.show();
+ }
+
+ public void importDatabase(){
+ File csvFile=new File(EXPORT_FILENAME);
+ if (!csvFile.exists()) {
+ String msg=getString(R.string.import_file_missing) + " " +
+ EXPORT_FILENAME;
+ Toast.makeText(CategoryList.this, msg,
+ Toast.LENGTH_LONG).show();
+ return;
+ }
+ Dialog about = new AlertDialog.Builder(this)
+ .setIcon(R.drawable.passicon)
+ .setTitle(R.string.dialog_import_title)
+ .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ deleteDatabase4Import();
+ }
+ })
+ .setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ importDeletedDatabase=false;
+ importDatabaseThreadStart();
+ }
+ })
+ .setMessage(R.string.dialog_import_msg)
+ .create();
+ about.show();
+ }
+
+ /**
+ * Start a separate thread to import the database. By running
+ * the import in a thread it allows the main UI thread to return
+ * and permit the updating of the progress dialog.
+ */
+ private void importDatabaseThreadStart(){
+ showDialog(IMPORT_PROGRESS_KEY);
+ importThread = new Thread(new Runnable() {
+ public void run() {
+ importDatabaseFromCSV();
+ dismissDialog(IMPORT_PROGRESS_KEY);
+
+ Message m = new Message();
+ m.what = CategoryList.MSG_IMPORT;
+ CategoryList.this.myViewUpdateHandler.sendMessage(m);
+
+ if (debug) Log.d(TAG,"thread end");
+ }
+ });
+ importThread.start();
+ }
+
+ /**
+ * While running inside a thread, read from a CSV and import
+ * into the database.
+ */
+ private void importDatabaseFromCSV(){
+ try {
+ importMessage="";
+ importedEntries=0;
+
+ final int recordLength=6;
+ CSVReader reader= new CSVReader(new FileReader(EXPORT_FILENAME));
+ String [] nextLine;
+ nextLine = reader.readNext();
+ if (nextLine==null) {
+ importMessage=getString(R.string.import_error_first_line);
+ return;
+ }
+ if (nextLine.length != recordLength){
+ importMessage=getString(R.string.import_error_first_line);
+ return;
+ }
+ if ((nextLine[0].compareToIgnoreCase(getString(R.string.category)) != 0) ||
+ (nextLine[1].compareToIgnoreCase(getString(R.string.description)) != 0) ||
+ (nextLine[2].compareToIgnoreCase(getString(R.string.website)) != 0) ||
+ (nextLine[3].compareToIgnoreCase(getString(R.string.username)) != 0) ||
+ (nextLine[4].compareToIgnoreCase(getString(R.string.password)) != 0) ||
+ (nextLine[5].compareToIgnoreCase(getString(R.string.notes)) != 0))
+ {
+ importMessage=getString(R.string.import_error_first_line);
+ return;
+ }
+// Log.i(TAG,"first line is valid");
+
+ HashMap categoryToId=getCategoryToId(dbHelper);
+ //
+ // take a pass through the CSV and collect any new Categories
+ //
+ HashMap categoriesFound = new HashMap();
+ int categoryCount=0;
+ int line=0;
+ while ((nextLine = reader.readNext()) != null) {
+ line++;
+ if (importThread.isInterrupted()) {
+ return;
+ }
+ // nextLine[] is an array of values from the line
+ if ((nextLine==null) || (nextLine[0]=="")){
+ continue; // skip blank categories
+ }
+ if (categoryToId.containsKey(nextLine[0])){
+ continue; // don't recreate existing categories
+ }
+// if (debug) Log.d(TAG,"line["+line+"] found category ("+nextLine[0]+")");
+ Long passwordsInCategory= new Long(1);
+ if (categoriesFound.containsKey(nextLine[0])) {
+ // we've seen this category before, bump its count
+ passwordsInCategory+=categoriesFound.get(nextLine[0]);
+ } else {
+ // newly discovered category
+ categoryCount++;
+ }
+ categoriesFound.put(nextLine[0], passwordsInCategory);
+ if (categoryCount>MAX_CATEGORIES){
+ importMessage=getString(R.string.import_too_many_categories);
+ return;
+ }
+ }
+ if (debug) Log.d(TAG,"found "+categoryCount+" categories");
+ if (categoryCount!=0)
+ {
+ Set categorySet = categoriesFound.keySet();
+ Iterator i=categorySet.iterator();
+ while (i.hasNext()){
+ addCategory(i.next());
+ }
+ }
+ reader.close();
+
+ categoryToId=getCategoryToId(dbHelper); // re-read the categories to get id's of new categories
+ //
+ // read the whole file again to import the actual fields
+ //
+ reader = new CSVReader(new FileReader(EXPORT_FILENAME));
+ nextLine = reader.readNext();
+ int newEntries=0;
+ int lineNumber=0;
+ String lineErrors="";
+ int lineErrorsCount=0;
+ final int maxLineErrors=10;
+ while ((nextLine = reader.readNext()) != null) {
+ lineNumber++;
+// Log.d(TAG,"lineNumber="+lineNumber);
+
+ if (importThread.isInterrupted()) {
+ return;
+ }
+
+ // nextLine[] is an array of values from the line
+ if (nextLine.length < 2){
+ if (lineErrorsCount < maxLineErrors) {
+ lineErrors += "line "+lineNumber+": "+
+ getString(R.string.import_not_enough_fields)+"\n";
+ lineErrorsCount++;
+ }
+ continue; // skip if not enough fields
+ }
+ if (nextLine.length < recordLength){
+ // if the fields after category and description are missing,
+ // just fill them in
+ String [] replacement=new String[recordLength];
+ for (int i=0;i getCategoryToId(DBHelper dbHelper)
+ {
+ CryptoHelper ch = new CryptoHelper();
+ if(masterKey == null) {
+ masterKey = "";
+ }
+ ch.setPassword(masterKey);
+
+ HashMap categories = new HashMap();
+ List rows;
+ rows = dbHelper.fetchAllCategoryRows();
+
+ for (CategoryEntry row : rows) {
+ String cryptDesc = row.name;
+ row.plainName = "";
+ try {
+ row.plainName = ch.decrypt(cryptDesc);
+ categories.put(row.plainName, row.id);
+ } catch (CryptoHelperException e) {
+ Log.e(TAG,e.toString());
+ }
+ }
+ return categories;
+ }
+}
\ No newline at end of file
diff --git a/src/org/openintents/safe/ChangePass.java b/src/org/openintents/safe/ChangePass.java
new file mode 100644
index 0000000..fd949f3
--- /dev/null
+++ b/src/org/openintents/safe/ChangePass.java
@@ -0,0 +1,379 @@
+/* $Id: ChangePass.java 71 2008-12-28 19:42:48Z peli0101 $
+ *
+ * Copyright 2008 Randy McEoin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openintents.safe;
+
+import java.util.List;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+/**
+ * Allows user to change the password used to unlock the application
+ * as well as encrypt the database.
+ *
+ * @author Randy McEoin
+ */
+public class ChangePass extends Activity {
+
+ private static boolean debug = false;
+ private static final String TAG = "ChangePass";
+
+ ProgressDialog mChangePassProgress;
+
+ private static final int CHANGE_PASS_PROGRESS_KEY = 0;
+
+ protected static final int MSG_PASS_CHANGED = 0x101;
+
+ Thread changePassThread = null;
+ String oldPassword;
+ String newPassword;
+
+ Handler myViewUpdateHandler = new Handler(){
+ // @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case ChangePass.MSG_PASS_CHANGED:
+ Toast.makeText(ChangePass.this, R.string.password_changed,
+ Toast.LENGTH_LONG).show();
+ finish();
+ break;
+ }
+ super.handleMessage(msg);
+ }
+ };
+
+ /**
+ * Called when the activity is first created.
+ */
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ if (debug) Log.d(TAG,"onCreate()");
+
+ if (CategoryList.isSignedIn()==false) {
+ finish();
+ }
+
+ setContentView(R.layout.chg_pass);
+ String title = getResources().getString(R.string.app_name) + " - " +
+ getResources().getString(R.string.change_password);
+ setTitle(title);
+
+ Button changePasswordButton = (Button) findViewById(R.id.change_password_button);
+
+ changePasswordButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View arg0) {
+ performChangePass();
+ }
+ });
+
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ if (debug) Log.d(TAG,"onPause()");
+
+ if ((changePassThread != null) && (changePassThread.isAlive())) {
+ if (debug) Log.d(TAG,"wait for thread");
+// importThread.interrupt();
+ int maxWaitToDie=500000;
+ try { changePassThread.join(maxWaitToDie); }
+ catch(InterruptedException e){} // ignore
+ }
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case CHANGE_PASS_PROGRESS_KEY: {
+ ProgressDialog dialog = new ProgressDialog(this);
+ dialog.setMessage("Please wait while re-encrypting...");
+ dialog.setIndeterminate(false);
+ dialog.setCancelable(false);
+ return dialog;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Check the old, new and verify fields then try to re-encrypt
+ * the data.
+ */
+ private void performChangePass() {
+ if (debug) Log.d(TAG,"performChangePass()");
+
+ EditText oldPassword = (EditText) findViewById(R.id.old_password);
+ EditText newPassword = (EditText) findViewById(R.id.new_password);
+ EditText verifyPassword = (EditText) findViewById(R.id.verify_password);
+
+ String oldPlain = oldPassword.getText().toString();
+ String newPlain = newPassword.getText().toString();
+ String verifyPlain = verifyPassword.getText().toString();
+
+ if (newPlain.compareTo(verifyPlain) != 0) {
+ Toast.makeText(ChangePass.this, R.string.new_verify_mismatch,
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ if (newPlain.length() < 4) {
+ Toast.makeText(ChangePass.this, R.string.notify_blank_pass,
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ if (!checkUserPassword(oldPlain)) {
+ Toast.makeText(ChangePass.this, R.string.invalid_old_password,
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+// changePassThreadStart(oldPlain, newPlain);
+ changeMasterPassword(oldPlain, newPlain);
+ }
+
+ /**
+ * Start a separate thread to import the database. By running
+ * the import in a thread it allows the main UI thread to return
+ * and permit the updating of the progress dialog.
+ *
+ * @param oldPass clear text old password
+ * @param newPass clear text new password
+ */
+ /*
+ private void changePassThreadStart(String oldPass, String newPass){
+ if (debug) Log.d(TAG,"changePassThreadStart(,)");
+ showDialog(CHANGE_PASS_PROGRESS_KEY);
+ oldPassword=oldPass;
+ newPassword=newPass;
+ changePassThread = new Thread(new Runnable() {
+ public void run() {
+ changePassword(oldPassword, newPassword);
+
+ dismissDialog(CHANGE_PASS_PROGRESS_KEY);
+
+ Message m = new Message();
+ m.what = ChangePass.MSG_PASS_CHANGED;
+ ChangePass.this.myViewUpdateHandler.sendMessage(m);
+
+ if (debug) Log.d(TAG,"thread end");
+ }
+ });
+ changePassThread.start();
+ }
+ */
+
+ private boolean changeMasterPassword(String oldPass, String newPass) {
+
+ DBHelper dbHelper= new DBHelper(this);
+
+ CryptoHelper ch = new CryptoHelper(CryptoHelper.EncryptionStrong);
+
+ String encryptedMasterKey = dbHelper.fetchMasterKey();
+ String decryptedMasterKey = "";
+ try {
+ ch.setPassword(oldPass);
+ decryptedMasterKey = ch.decrypt(encryptedMasterKey);
+ if (ch.getStatus()==true) { // successful decryption?
+ ch.setPassword(newPass);
+ encryptedMasterKey = ch.encrypt(decryptedMasterKey);
+ if (ch.getStatus()==true) { // successful encryption?
+ dbHelper.storeMasterKey(encryptedMasterKey);
+ dbHelper.close();
+ Toast.makeText(ChangePass.this, R.string.password_changed,
+ Toast.LENGTH_LONG).show();
+ setResult(RESULT_OK);
+ finish();
+ return true;
+ }
+ }
+
+ } catch (CryptoHelperException e) {
+ Log.e(TAG, e.toString());
+ }
+
+ dbHelper.close();
+
+ Toast.makeText(ChangePass.this, R.string.error_changing_password,
+ Toast.LENGTH_LONG).show();
+ return false;
+ }
+
+ /**
+ * This is an older function. We'll want to re-use this when we
+ * allow the user to regenerate the master key.
+ *
+ * @param oldPass
+ * @param newPass
+ */
+ public void changePassword(String oldPass, String newPass) {
+ if (debug) Log.d(TAG,"changePassword(,)");
+
+ DBHelper dbHelper= new DBHelper(this);
+
+ CryptoHelper ch = new CryptoHelper();
+
+ List categoryRows;
+ categoryRows = dbHelper.fetchAllCategoryRows();
+
+ List passRows;
+ passRows = dbHelper.fetchAllRows(new Long(0));
+
+ /**
+ * Decrypt everything using the old password.
+ */
+ if (debug) Log.d(TAG,"decrypting");
+ ch.setPassword(oldPass);
+
+ for (CategoryEntry row : categoryRows) {
+ row.plainName = "";
+ try {
+ row.plainName = ch.decrypt(row.name);
+ } catch (CryptoHelperException e) {
+ if (debug) Log.e(TAG,e.toString());
+ Toast.makeText(ChangePass.this, e.toString(),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ }
+
+ for (PassEntry row : passRows) {
+ try {
+ row.plainDescription = ch.decrypt(row.description);
+ row.plainWebsite = ch.decrypt(row.website);
+ row.plainUsername = ch.decrypt(row.username);
+ row.plainPassword = ch.decrypt(row.password);
+ row.plainNote = ch.decrypt(row.note);
+ } catch (CryptoHelperException e) {
+ if (debug) Log.e(TAG,e.toString());
+ Toast.makeText(ChangePass.this, e.toString(),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ }
+
+ /**
+ * Encrypt everything using the new password.
+ */
+ if (debug) Log.d(TAG,"encrypting");
+ ch.setPassword(newPass);
+
+ for (CategoryEntry row : categoryRows) {
+ try {
+ row.name = ch.encrypt(row.plainName);
+ } catch (CryptoHelperException e) {
+ if (debug) Log.e(TAG,e.toString());
+ Toast.makeText(ChangePass.this, e.toString(),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ }
+
+ for (PassEntry row : passRows) {
+ try {
+ row.description = ch.encrypt(row.plainDescription);
+ row.website = ch.encrypt(row.plainWebsite);
+ row.username = ch.encrypt(row.plainUsername);
+ row.password = ch.encrypt(row.plainPassword);
+ row.note = ch.encrypt(row.plainNote);
+ } catch (CryptoHelperException e) {
+ if (debug) Log.e(TAG,e.toString());
+ Toast.makeText(ChangePass.this, e.toString(),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ }
+
+ /**
+ * Update the database with the newly encrypted data.
+ */
+ if (debug) Log.d(TAG,"updating database");
+ dbHelper.beginTransaction();
+
+ for (CategoryEntry row : categoryRows) {
+ dbHelper.updateCategory(row.id, row);
+ }
+
+ for (PassEntry row : passRows) {
+ dbHelper.updatePassword(row.id, row);
+ }
+
+ byte[] md5Key = CryptoHelper.md5String(newPass);
+ String hexKey = CryptoHelper.toHexString(md5Key);
+ String cryptKey = "";
+ Log.i(TAG, "Saving Password: " + hexKey);
+ try {
+ cryptKey = ch.encrypt(hexKey);
+ dbHelper.storeMasterKey(cryptKey);
+ } catch (CryptoHelperException e) {
+ Log.e(TAG, e.toString());
+ Toast.makeText(ChangePass.this, e.toString(),
+ Toast.LENGTH_SHORT).show();
+ dbHelper.rollback();
+ dbHelper.close();
+ return;
+ }
+
+ dbHelper.commit();
+
+ PassList.setMasterKey(newPass);
+ CategoryList.setMasterKey(newPass);
+
+ dbHelper.close();
+ }
+
+ /**
+ * Check the provided clear text password with the one stored
+ * in the database.
+ *
+ * @param pass = clear text password
+ * @return True if password is correct.
+ */
+ private boolean checkUserPassword(String pass) {
+ if (debug) Log.d(TAG,"checkUserPassword()");
+
+ DBHelper dbHelper= new DBHelper(this);
+ String confirmKey = dbHelper.fetchMasterKey();
+
+ CryptoHelper ch = new CryptoHelper(CryptoHelper.EncryptionStrong);
+ ch.setPassword(pass);
+
+ try {
+ ch.decrypt(confirmKey);
+ } catch (CryptoHelperException e) {
+ Log.e(TAG, e.toString());
+ }
+ dbHelper.close();
+
+ // was decryption of the master key successful?
+ if (ch.getStatus()==true) {
+ return true; // then we must have a good master password
+ }
+ return false;
+ }
+
+}
diff --git a/src/org/openintents/safe/CryptoHelper.java b/src/org/openintents/safe/CryptoHelper.java
new file mode 100644
index 0000000..56736cd
--- /dev/null
+++ b/src/org/openintents/safe/CryptoHelper.java
@@ -0,0 +1,316 @@
+/* $Id: CryptoHelper.java 78 2008-12-31 21:47:07Z peli0101 $
+ *
+ * Copyright 2007-2008 Steven Osborn
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openintents.safe;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.security.DigestInputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.spec.InvalidKeySpecException;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.KeyGenerator;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.PBEParameterSpec;
+
+//import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+import android.util.Log;
+
+/**
+ * Crypto helper class.
+ *
+ * Basic crypto class that uses Bouncy Castle Provider to
+ * encrypt/decrypt data using PBE (Password Based Encryption) via
+ * 128Bit AES. I'm fairly new to both Crypto and Java so if you
+ * notice I've done something terribly wrong here please let me
+ * know.
+ *
+ * @author Steven Osborn - http://steven.bitsetters.com
+ */
+public class CryptoHelper {
+
+ private static String TAG = "CryptoHelper";
+ protected static PBEKeySpec pbeKeySpec;
+ protected static PBEParameterSpec pbeParamSpec;
+ protected static SecretKeyFactory keyFac;
+
+ public final static int EncryptionMedium=1;
+ public final static int EncryptionStrong=2;
+
+ protected static String algorithmMedium = "PBEWithMD5And128BitAES-CBC-OpenSSL";
+// protected static String algorithm = "PBEWithSHA1And128BitAES-CBC-BC"; // slower
+ protected static String algorithmStrong = "PBEWithSHA1And256BitAES-CBC-BC";
+ private String algorithm = "";
+ protected static String desAlgorithm = "DES";
+ protected static String password = null;
+ protected static SecretKey pbeKey;
+ protected static Cipher pbeCipher;
+ private boolean status=false; // status of the last encrypt/decrypt
+
+ private static final byte[] salt = {
+ (byte)0xfc, (byte)0x76, (byte)0x80, (byte)0xae,
+ (byte)0xfd, (byte)0x82, (byte)0xbe, (byte)0xee,
+ };
+
+ private static final int count = 20;
+
+ /**
+ * Constructor which defaults to a medium encryption level.
+ */
+ public CryptoHelper() {
+ initialize(EncryptionMedium);
+ }
+ /**
+ * Constructor which allows the specification of the encryption level.
+ *
+ * @param Strength encryption strength
+ */
+ public CryptoHelper(int Strength) {
+ initialize(Strength);
+ }
+ /**
+ * Initialize the class. Sets the encryption level for the instance
+ * and generates the secret key factory.
+ *
+ * @param Strength
+ */
+ private void initialize(int Strength) {
+ switch (Strength) {
+ case EncryptionMedium:
+ algorithm=algorithmMedium;
+ break;
+ case EncryptionStrong:
+ algorithm=algorithmStrong;
+ break;
+ }
+ pbeParamSpec = new PBEParameterSpec(salt,count);
+ try {
+ keyFac = SecretKeyFactory
+ .getInstance(algorithm,"BC");
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(TAG,"CryptoHelper(): "+e.toString());
+ } catch (NoSuchProviderException e) {
+ Log.e(TAG,"CryptoHelper(): "+e.toString());
+ }
+ }
+ /**
+ * @author Isaac Potoczny-Jones
+ *
+ * @return null if failure, otherwise hex string version of key
+ */
+ public static String generateMasterKey () {
+ try {
+ KeyGenerator keygen;
+ keygen = KeyGenerator.getInstance("AES");
+ keygen.init(256);
+ SecretKey genDesKey = keygen.generateKey();
+ return toHexString(genDesKey.getEncoded());
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(TAG,"generateMasterKey(): "+e.toString());
+ }
+ return null; //error case.
+ }
+
+ /**
+ *
+ * @param message
+ * @return
+ * @throws NoSuchAlgorithmException
+ * @throws IOException
+ */
+ public static byte[] md5String(String message) {
+
+ byte[] input = message.getBytes();
+
+ MessageDigest hash;
+ ByteArrayInputStream bIn = null;
+ DigestInputStream dIn = null;
+
+ try {
+ hash = MessageDigest.getInstance("MD5");
+
+ bIn = new ByteArrayInputStream(input);
+ dIn = new DigestInputStream(bIn, hash);
+
+ for(int i=0;i 0) {
+ vc.moveToFirst();
+ version=vc.getInt(0);
+ }
+ vc.close();
+ if (version!=DATABASE_VERSION) {
+ needsUpgrade=true;
+ Log.e(TAG,"database version mismatch");
+ }
+ }
+ c.close();
+
+
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ }
+
+ private void CreateDatabase(SQLiteDatabase db)
+ {
+ try {
+ db.execSQL(DBVERSION_CREATE);
+ ContentValues args = new ContentValues();
+ args.put("version", DATABASE_VERSION);
+ db.insert(TABLE_DBVERSION, null, args);
+
+ db.execSQL(CATEGORIES_CREATE);
+ needsPrePopulation=true;
+
+ db.execSQL(PASSWORDS_CREATE);
+ db.execSQL(PACKAGE_ACCESS_CREATE);
+ db.execSQL(MASTER_KEY_CREATE);
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ }
+
+ public void deleteDatabase()
+ {
+ try {
+ db.execSQL(PASSWORDS_DROP);
+ db.execSQL(PASSWORDS_CREATE);
+
+ db.execSQL(CATEGORIES_DROP);
+ db.execSQL(CATEGORIES_CREATE);
+
+ db.execSQL(PACKAGE_ACCESS_DROP);
+ db.execSQL(PACKAGE_ACCESS_CREATE);
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ }
+
+ public boolean needsUpgrade()
+ {
+ return needsUpgrade;
+ }
+
+ public boolean getPrePopulate()
+ {
+ return needsPrePopulation;
+ }
+
+ public void clearPrePopulate()
+ {
+ needsPrePopulation=false;
+ }
+ /**
+ * Close database connection
+ */
+ public void close() {
+ try {
+ db.close();
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"close exception: " + e.getLocalizedMessage());
+ }
+ }
+
+ public int fetchVersion() {
+ int version=0;
+ try {
+ Cursor c = db.query(true, TABLE_DBVERSION,
+ new String[] {"version"},
+ null, null, null, null, null,null);
+ if(c.getCount() > 0) {
+ c.moveToFirst();
+ version=c.getInt(0);
+ }
+ c.close();
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ return version;
+ }
+
+ public String fetchOldConfirm() {
+ String key="";
+ try {
+ Cursor c = db.query(true, TABLE_VERIFY, new String[] {"confirm"},
+ null, null, null, null, null,null);
+ if(c.getCount() > 0) {
+ c.moveToFirst();
+ key=c.getString(0);
+ }
+ c.close();
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ return key;
+ }
+ /**
+ *
+ * @return
+ */
+ public String fetchMasterKey() {
+ String key="";
+ try {
+ Cursor c = db.query(true, TABLE_MASTER_KEY, new String[] {"encryptedkey"},
+ null, null, null, null, null,null);
+ if(c.getCount() > 0) {
+ c.moveToFirst();
+ key=c.getString(0);
+ }
+ c.close();
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ return key;
+ }
+
+ /**
+ *
+ * @param PBEKey
+ */
+ public void storeMasterKey(String MasterKey) {
+ ContentValues args = new ContentValues();
+ try {
+ db.delete(TABLE_MASTER_KEY, "1=1", null);
+ args.put("encryptedkey", MasterKey);
+ db.insert(TABLE_MASTER_KEY, null, args);
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ }
+
+//////////Category Functions ////////////////
+
+ /**
+ * Doesn't add the category if it already exists.
+ * @param entry
+ * @returns row id
+ */
+ public long addCategory(CategoryEntry entry) {
+ ContentValues initialValues = new ContentValues();
+
+ long rowID=-1;
+ Cursor c =
+ db.query(true, TABLE_CATEGORIES, new String[] {
+ "id", "name"}, "name='" + entry.name + "'" , null, null, null, null, null);
+ if (c.getCount() > 0) {
+ c.moveToFirst();
+ rowID = c.getLong(0);
+
+ } else {// there's not already such a category...
+ initialValues.put("name", entry.name);
+
+ try {
+ rowID=db.insert(TABLE_CATEGORIES, null, initialValues);
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ }
+ c.close();
+ return rowID;
+ }
+
+ /**
+ *
+ * @param Id
+ */
+ public void deleteCategory(long Id) {
+ try {
+ db.delete(TABLE_CATEGORIES, "id=" + Id, null);
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ }
+
+ /**
+ *
+ * @return
+ */
+ public List fetchAllCategoryRows(){
+ ArrayList ret = new ArrayList();
+ try {
+ Cursor c =
+ db.query(TABLE_CATEGORIES, new String[] {
+ "id", "name"},
+ null, null, null, null, null);
+ int numRows = c.getCount();
+ c.moveToFirst();
+ for (int i = 0; i < numRows; ++i) {
+ CategoryEntry row = new CategoryEntry();
+ row.id = c.getLong(0);
+ row.name = c.getString(1);
+ ret.add(row);
+ c.moveToNext();
+ }
+ c.close();
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ return ret;
+ }
+
+ /**
+ *
+ * @param Id
+ * @return
+ */
+ public CategoryEntry fetchCategory(long Id) {
+ CategoryEntry row = new CategoryEntry();
+ try {
+ Cursor c =
+ db.query(true, TABLE_CATEGORIES, new String[] {
+ "id", "name"}, "id=" + Id, null, null, null, null, null);
+ if (c.getCount() > 0) {
+ c.moveToFirst();
+ row.id = c.getLong(0);
+
+ row.name = c.getString(1);
+ } else {
+ row.id = -1;
+ row.name = null;
+ }
+ c.close();
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ return row;
+ }
+
+ /**
+ *
+ * @param Id
+ * @param entry
+ */
+ public void updateCategory(long Id, CategoryEntry entry) {
+ ContentValues args = new ContentValues();
+ args.put("name", entry.name);
+
+ try {
+ db.update(TABLE_CATEGORIES, args, "id=" + Id, null);
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ }
+
+
+////////// Password Functions ////////////////
+
+
+ /**
+ *
+ * @param categoryId
+ */
+ public int countPasswords(long categoryId) {
+ int count=0;
+ try {
+ Cursor c = db.query(TABLE_PASSWORDS, new String[] {
+ "count(*)"},
+ "category="+categoryId, null, null, null, null);
+ c.moveToFirst();
+ count=c.getInt(0);
+ c.close();
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ Log.i(TAG,"count="+count);
+ return count;
+ }
+
+ /**
+ *
+ * @return
+ */
+ public List fetchAllRows(Long CategoryId){
+ ArrayList ret = new ArrayList();
+ try {
+ Cursor c;
+ if (CategoryId==0)
+ {
+ c = db.query(TABLE_PASSWORDS, new String[] {
+ "id", "password", "description", "username", "website", "note", "category", "unique_name"},
+ null, null, null, null, null);
+ } else {
+ c = db.query(TABLE_PASSWORDS, new String[] {
+ "id", "password", "description", "username", "website", "note", "category", "unique_name"},
+ "category="+CategoryId, null, null, null, null);
+ }
+ int numRows = c.getCount();
+ c.moveToFirst();
+ for (int i = 0; i < numRows; ++i) {
+ PassEntry row = new PassEntry();
+ row.id = c.getLong(0);
+
+ row.password = c.getString(1);
+ row.description = c.getString(2);
+ row.username = c.getString(3);
+ row.website = c.getString(4);
+ row.note = c.getString(5);
+
+ row.category = c.getLong(6);
+ row.uniqueName = c.getString(7);
+
+ ret.add(row);
+ c.moveToNext();
+ }
+ c.close();
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ return ret;
+ }
+
+ /**
+ *
+ * @param Id
+ * @return
+ */
+ public PassEntry fetchPassword(long Id) {
+ PassEntry row = new PassEntry();
+ try {
+ Cursor c =
+ db.query(true, TABLE_PASSWORDS, new String[] {
+ "id", "password", "description", "username", "website",
+ "note", "category, unique_name"}, "id=" + Id, null, null, null, null, null);
+ if (c.getCount() > 0) {
+ c.moveToFirst();
+ row.id = c.getLong(0);
+
+ row.password = c.getString(1);
+ row.description = c.getString(2);
+ row.username = c.getString(3);
+ row.website = c.getString(4);
+ row.note = c.getString(5);
+
+ row.category = c.getLong(6);
+ row.uniqueName = c.getString(7);
+ } else {
+ row.id = -1;
+ row.description = row.password = null;
+ }
+ c.close();
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ return row;
+ }
+
+ public PassEntry fetchPassword(String uniqueName) {
+ PassEntry row = new PassEntry();
+ row.id = -1;
+ row.description = row.password = null;
+ try {
+ Cursor c =
+ db.query(true, TABLE_PASSWORDS, new String[] {
+ "id", "password", "description", "username", "website",
+ "note", "category", "unique_name"}, "unique_name='" + uniqueName + "'",
+ null, null, null, null, null);
+ if (c.getCount() > 0) {
+ c.moveToFirst();
+ row.id = c.getLong(0);
+
+ row.password = c.getString(1);
+ row.description = c.getString(2);
+ row.username = c.getString(3);
+ row.website = c.getString(4);
+ row.note = c.getString(5);
+
+ row.category = c.getLong(6);
+ row.uniqueName = c.getString(7);
+ }
+ c.close();
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ return row;
+ }
+
+
+ public ArrayList fetchPackageAccess (long passwordID) {
+ ArrayList pkgs = new ArrayList();
+ Cursor c = null;
+ try {
+ c =
+ db.query(true, TABLE_PACKAGE_ACCESS, new String[] {
+ "package"}, "id=" + passwordID,
+ null, null, null, null, null);
+ if (c.getCount() > 0) {
+ c.moveToFirst();
+ while (! c.isAfterLast()) {
+ pkgs.add(c.getString(0));
+ c.moveToNext();
+ }
+ }
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ } finally {
+ if (c != null) c.close();
+ }
+
+ return pkgs;
+ }
+
+ public void addPackageAccess (long passwordID, String packageToAdd) {
+ ContentValues packageAccessValues = new ContentValues ();
+ packageAccessValues.put("id", passwordID);
+ packageAccessValues.put("package", packageToAdd);
+ try {
+ db.insert(TABLE_PACKAGE_ACCESS, null, packageAccessValues);
+ } catch (SQLException e) {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ }
+
+ /**
+ *
+ * @param Id
+ * @param entry
+ */
+ public void updatePassword(long Id, PassEntry entry) {
+ ContentValues args = new ContentValues();
+ args.put("description", entry.description);
+ args.put("username", entry.username);
+ args.put("password", entry.password);
+ args.put("website", entry.website);
+ args.put("note", entry.note);
+ args.put("unique_name", entry.uniqueName);
+ try {
+ db.update(TABLE_PASSWORDS, args, "id=" + Id, null);
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ }
+
+ /**
+ * Only update the category field of the password entry.
+ *
+ * @param Id the id of the password entry
+ * @param newCategoryId the updated category id
+ */
+ public void updatePasswordCategory(long Id, long newCategoryId) {
+ if (Id<0 || newCategoryId<0) {
+ //make sure values appear valid
+ return;
+ }
+ ContentValues args = new ContentValues();
+ args.put("category", newCategoryId);
+
+ try {
+ db.update(TABLE_PASSWORDS, args, "id=" + Id, null);
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ }
+
+ /**
+ *
+ * @param entry
+ */
+ public long addPassword(PassEntry entry) {
+ long id = -1;
+ ContentValues initialValues = new ContentValues();
+ initialValues.put("category", entry.category);
+ initialValues.put("password", entry.password);
+ initialValues.put("description", entry.description);
+ initialValues.put("username",entry.username);
+ initialValues.put("website", entry.website);
+ initialValues.put("note", entry.note);
+ initialValues.put("unique_name", entry.uniqueName);
+
+ try {
+ id = db.insert(TABLE_PASSWORDS, null, initialValues);
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ return (id);
+ }
+
+ /**
+ *
+ * @param Id
+ */
+ public void deletePassword(long Id) {
+ try {
+ db.delete(TABLE_PASSWORDS, "id=" + Id, null);
+ db.delete(TABLE_PACKAGE_ACCESS, "id=" + Id, null);
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ }
+ /**
+ * Begin a transaction on an open database.
+ *
+ * @return true if successful
+ */
+ public boolean beginTransaction() {
+ try {
+ db.execSQL("begin transaction;");
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Commit all changes since the begin transaction on an
+ * open database.
+ */
+ public void commit() {
+ try {
+ db.execSQL("commit;");
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ }
+
+ /**
+ * Rollback all changes since the begin transaction on an
+ * open database.
+ */
+ public void rollback() {
+ try {
+ db.execSQL("rollback;");
+ } catch (SQLException e)
+ {
+ Log.d(TAG,"SQLite exception: " + e.getLocalizedMessage());
+ }
+ }
+}
+
diff --git a/src/org/openintents/safe/FrontDoor.java b/src/org/openintents/safe/FrontDoor.java
new file mode 100644
index 0000000..74d3c6f
--- /dev/null
+++ b/src/org/openintents/safe/FrontDoor.java
@@ -0,0 +1,408 @@
+/* $Id: FrontDoor.java 98 2009-01-15 04:33:59Z rmceoin $
+ *
+ * Copyright 2007-2008 Steven Osborn
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openintents.safe;
+
+
+import java.util.ArrayList;
+
+import org.openintents.intents.CryptoIntents;
+import org.openintents.safe.service.ServiceDispatch;
+import org.openintents.safe.service.ServiceDispatchImpl;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.widget.Toast;
+
+
+/**
+ * FrontDoor Activity
+ *
+ * This activity just acts as a splash screen and gets the password from the
+ * user that will be used to decrypt/encrypt password entries.
+ *
+ * @author Steven Osborn - http://steven.bitsetters.com
+ */
+public class FrontDoor extends Activity {
+
+ private boolean debug = false;
+ private static String TAG = "FrontDoor";
+
+ private DBHelper dbHelper;
+ private String masterKey;
+ private CryptoHelper ch;
+
+ // service elements
+ private ServiceDispatch service;
+ private ServiceDispatchConnection conn;
+
+ SharedPreferences mPreferences;
+ //public static String SERVICE_NAME = "org.openintents.safe.service.ServiceDispatchImpl";
+
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ initService(); // start up the PWS service so other applications can query.
+ }
+
+ //currently only handles result from askPassword function.
+ protected void onActivityResult (int requestCode, int resultCode, Intent data) {
+ switch (resultCode) {
+ case RESULT_OK:
+ masterKey = data.getStringExtra("masterKey");
+ String timeout = mPreferences.getString("lock_timeout", "5");
+ int timeoutMinutes=5; // default to 5
+ try {
+ timeoutMinutes = Integer.valueOf(timeout);
+ } catch (NumberFormatException e) {
+ Log.d(TAG,"why is lock_timeout busted?");
+ }
+ try {
+ service.setTimeoutMinutes(timeoutMinutes);
+ service.setPassword(masterKey); // should already be connected.
+ } catch (RemoteException e1) {
+ // TODO Auto-generated catch block
+ e1.printStackTrace();
+ }
+ actionDispatch();
+ break;
+ case RESULT_CANCELED:
+ setResult(RESULT_CANCELED);
+ finish();
+ break;
+ }
+ }
+
+ protected void actionDispatch () {
+ final Intent thisIntent = getIntent();
+ final String action = thisIntent.getAction();
+ Intent callbackIntent = getIntent();
+ int callbackResult = RESULT_CANCELED;
+ PassList.setMasterKey(masterKey);
+ CategoryList.setMasterKey(masterKey);
+ if (ch == null) {
+ ch = new CryptoHelper(CryptoHelper.EncryptionMedium);
+ ch.setPassword(masterKey);
+ }
+
+ boolean externalAccess = mPreferences.getBoolean("external_access", false);
+
+ if (action == null || action.equals(Intent.ACTION_MAIN)){
+ //TODO: When launched from debugger, action is null. Other such cases?
+ Intent i = new Intent(getApplicationContext(),
+ CategoryList.class);
+ startActivity(i);
+ } else if (externalAccess){
+
+ // which action?
+ if (action.equals (CryptoIntents.ACTION_ENCRYPT)) {
+ callbackResult = encryptIntent(thisIntent, callbackIntent);
+ } else if (action.equals (CryptoIntents.ACTION_DECRYPT)) {
+ callbackResult = decryptIntent(thisIntent, callbackIntent);
+ } else if (action.equals (CryptoIntents.ACTION_GET_PASSWORD)
+ || action.equals (CryptoIntents.ACTION_SET_PASSWORD)) {
+ try {
+ callbackIntent = getSetPassword (thisIntent, callbackIntent);
+ callbackResult = RESULT_OK;
+ } catch (CryptoHelperException e) {
+ Log.e(TAG, e.toString());
+ Toast.makeText(FrontDoor.this,
+ "There was a crypto error while retreiving the requested password: " + e.getMessage(),
+ Toast.LENGTH_SHORT).show();
+ } catch (Exception e) {
+ Log.e(TAG, e.toString());
+ //TODO: Turn this into a proper error dialog.
+ Toast.makeText(FrontDoor.this,
+ "There was an error in retreiving the requested password: " + e.getMessage(),
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ setResult(callbackResult, callbackIntent);
+ }
+ finish();
+ }
+
+
+ /**
+ * Encrypt all supported fields in the intent and return the result in callbackIntent.
+ *
+ * @param thisIntent
+ * @param callbackIntent
+ * @return callbackResult
+ */
+ private int encryptIntent(final Intent thisIntent, Intent callbackIntent) {
+ int callbackResult = RESULT_CANCELED;
+ try {
+ if (thisIntent.hasExtra(CryptoIntents.EXTRA_TEXT)) {
+ // get the body text out of the extras.
+ String inputBody = thisIntent.getStringExtra (CryptoIntents.EXTRA_TEXT);
+ String outputBody = "";
+ outputBody = ch.encrypt (inputBody);
+ // stash the encrypted text in the extra
+ callbackIntent.putExtra(CryptoIntents.EXTRA_TEXT, outputBody);
+ }
+
+ if (thisIntent.hasExtra(CryptoIntents.EXTRA_TEXT_ARRAY)) {
+ String[] in = thisIntent.getStringArrayExtra(CryptoIntents.EXTRA_TEXT_ARRAY);
+ String[] out = new String[in.length];
+ for (int i = 0; i < in.length; i++) {
+ if (in[i] != null) {
+ out[i] = ch.encrypt(in[i]);
+ }
+ }
+ callbackIntent.putExtra(CryptoIntents.EXTRA_TEXT_ARRAY, out);
+ }
+
+ // Support for binary fields could be added here (like images?)
+
+ callbackResult = RESULT_OK;
+ } catch (CryptoHelperException e) {
+ Log.e(TAG, e.toString());
+ }
+ return callbackResult;
+ }
+
+ /**
+ * Decrypt all supported fields in the intent and return the result in callbackIntent.
+ *
+ * @param thisIntent
+ * @param callbackIntent
+ * @return callbackResult
+ */
+ private int decryptIntent(final Intent thisIntent, Intent callbackIntent) {
+ int callbackResult = RESULT_CANCELED;
+ try {
+
+ if (thisIntent.hasExtra(CryptoIntents.EXTRA_TEXT)) {
+ // get the body text out of the extras.
+ String inputBody = thisIntent.getStringExtra (CryptoIntents.EXTRA_TEXT);
+ String outputBody = "";
+ outputBody = ch.decrypt (inputBody);
+ // stash the encrypted text in the extra
+ callbackIntent.putExtra(CryptoIntents.EXTRA_TEXT, outputBody);
+ }
+
+ if (thisIntent.hasExtra(CryptoIntents.EXTRA_TEXT_ARRAY)) {
+ String[] in = thisIntent.getStringArrayExtra(CryptoIntents.EXTRA_TEXT_ARRAY);
+ String[] out = new String[in.length];
+ for (int i = 0; i < in.length; i++) {
+ if (in[i] != null) {
+ out[i] = ch.decrypt(in[i]);
+ }
+ }
+ callbackIntent.putExtra(CryptoIntents.EXTRA_TEXT_ARRAY, out);
+ }
+
+ // Support for binary fields could be added here (like images?)
+
+ callbackResult = RESULT_OK;
+ } catch (CryptoHelperException e) {
+ Log.e(TAG, e.toString());
+ }
+ return callbackResult;
+ }
+
+ private Intent getSetPassword (Intent thisIntent, Intent callbackIntent) throws CryptoHelperException, Exception {
+ String action = thisIntent.getAction();
+ //TODO: Consider moving this elsewhere. Maybe DBHelper? Also move strings to resource.
+ //DBHelper dbHelper = new DBHelper(this);
+ Log.d(TAG, "GET_or_SET_PASSWORD");
+ String username = null;
+ String password = null;
+
+ String clearUniqueName = thisIntent.getStringExtra (CryptoIntents.EXTRA_UNIQUE_NAME);
+
+ if (clearUniqueName == null) throw new Exception ("EXTRA_UNIQUE_NAME not set.");
+
+ String uniqueName = ch.encrypt(clearUniqueName);
+ PassEntry row = dbHelper.fetchPassword(uniqueName);
+ boolean passExists = row.id > 1;
+
+ String clearCallingPackage = getCallingPackage();
+ String callingPackage = ch.encrypt (clearCallingPackage);
+ if (passExists) { // check for permission to access this password.
+ ArrayList packageAccess = dbHelper.fetchPackageAccess(row.id);
+ if (! PassEntry.checkPackageAccess(packageAccess, callingPackage)) {
+ throw new Exception ("It is currently not permissible for this application to request this password.");
+ }
+ /*TODO: check if this package is in the package_access table corresponding to this password:
+ * "Application 'org.syntaxpolice.ServiceTest' wants to access the
+ password for 'opensocial'.
+ [ ] Grant access this time.
+ [ ] Always grant access.
+ [ ] Always grant access to all passwords in org.syntaxpolice.ServiceTest category?
+ [ ] Don't grant access"
+ */
+ }
+
+ if (action.equals (CryptoIntents.ACTION_GET_PASSWORD)) {
+ if (passExists) {
+ username = ch.decrypt(row.username);
+ password = ch.decrypt(row.password);
+ } else throw new Exception ("Could not find password with the unique name: " + clearUniqueName);
+
+ // stashing the return values:
+ callbackIntent.putExtra(CryptoIntents.EXTRA_USERNAME, username);
+ callbackIntent.putExtra(CryptoIntents.EXTRA_PASSWORD, password);
+ } else if (action.equals (CryptoIntents.ACTION_SET_PASSWORD)) {
+ String clearUsername = thisIntent.getStringExtra (CryptoIntents.EXTRA_USERNAME);
+ String clearPassword = thisIntent.getStringExtra (CryptoIntents.EXTRA_PASSWORD);
+ if (clearPassword == null) {
+ throw new Exception ("PASSWORD extra must be set.");
+ }
+ row.username = ch.encrypt(clearUsername == null ? "" : clearUsername);
+ row.password = ch.encrypt(clearPassword);
+ // since this package is setting the password, it automatically gets access to it:
+ if (passExists) { //exists already
+ if (clearUsername.equals("") && clearPassword.equals("")) {
+ dbHelper.deletePassword(row.id);
+ } else {
+ dbHelper.updatePassword(row.id, row);
+ }
+ } else {// add a new one
+ row.uniqueName = uniqueName;
+ row.description=uniqueName; //for display purposes
+ // TODO: Should we send these fields in extras also? If so, probably not using
+ // the openintents namespace? If another application were to implement a keystore
+ // they might not want to use these.
+ row.website = "";
+ row.note = "";
+
+ String category = ch.encrypt("Application Data");
+ CategoryEntry c = new CategoryEntry();
+ c.name = category;
+ row.category = dbHelper.addCategory(c); //doesn't add category if it already exists
+ row.id = dbHelper.addPassword(row);
+ }
+ dbHelper.addPackageAccess(row.id, callingPackage);//already encrypted
+
+ }
+ return (callbackIntent);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ if (debug)
+ Log.d(TAG, "onPause()");
+
+ releaseService();
+ dbHelper.close();
+ dbHelper = null;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onPause();
+
+ if (debug)
+ Log.d(TAG, "onResume()");
+ if (dbHelper == null) {
+ dbHelper = new DBHelper(this);
+ }
+
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ releaseService();
+ }
+
+
+ //--------------------------- service stuff ------------
+ private void initService() {
+
+ String action = getIntent().getAction();
+ boolean isLocal = action == null || action.equals(Intent.ACTION_MAIN);
+ conn = new ServiceDispatchConnection(isLocal);
+ Intent i = new Intent();
+ i.setClass(this, ServiceDispatchImpl.class);
+ startService(i);
+ bindService( i, conn, Context.BIND_AUTO_CREATE);
+ }
+
+ private void releaseService() {
+ if (conn != null ) {
+ unbindService( conn );
+ conn = null;
+ }
+ }
+
+ class ServiceDispatchConnection implements ServiceConnection
+ {
+ boolean askPassIsLocal = false;
+ public ServiceDispatchConnection (Boolean isLocal) {
+ askPassIsLocal = isLocal;
+ }
+ public void onServiceConnected(ComponentName className,
+ IBinder boundService )
+ {
+ service = ServiceDispatch.Stub.asInterface((IBinder)boundService);
+
+ boolean promptforpassword = getIntent().getBooleanExtra(CryptoIntents.EXTRA_PROMPT, true);
+
+ try {
+ if (service.getPassword() == null) {
+ if (promptforpassword) {
+ // the service isn't running
+ Intent askPass = new Intent(getApplicationContext(),
+ AskPassword.class);
+
+ final Intent thisIntent = getIntent();
+ String inputBody = thisIntent.getStringExtra (CryptoIntents.EXTRA_TEXT);
+
+ askPass.putExtra (CryptoIntents.EXTRA_TEXT, inputBody);
+ askPass.putExtra (AskPassword.EXTRA_IS_LOCAL, askPassIsLocal);
+ //TODO: Is there a way to make sure all the extras are set?
+ startActivityForResult (askPass, 0);
+ } else {
+ // Don't prompt but cancel
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+
+ } else {
+ //service already started, so don't need to ask pw.
+ masterKey = service.getPassword();
+ actionDispatch();
+ }
+ } catch (RemoteException e) {
+ Log.d(TAG, e.toString());
+ }
+ Log.d( TAG,"onServiceConnected" );
+ }
+
+ public void onServiceDisconnected(ComponentName className)
+ {
+ service = null;
+ Log.d( TAG,"onServiceDisconnected" );
+ }
+ };
+
+}
diff --git a/src/org/openintents/safe/Help.java b/src/org/openintents/safe/Help.java
new file mode 100644
index 0000000..08f3e2c
--- /dev/null
+++ b/src/org/openintents/safe/Help.java
@@ -0,0 +1,112 @@
+/* $Id: Help.java 71 2008-12-28 19:42:48Z peli0101 $
+ *
+ * Copyright 2008 Steven Osborn and Randy McEoin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openintents.safe;
+
+import java.io.IOException;
+import java.io.InputStream;
+import android.app.Activity;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.webkit.WebView;
+
+/**
+ * This activity shows a help dialog to describe the application.
+ *
+ * @author Randy McEoin
+ */
+public class Help extends Activity {
+
+ private static boolean debug = false;
+ private static String TAG = "Help";
+
+ // Menu Item order
+ public static final int CLOSE_HELP_INDEX = Menu.FIRST;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ //Setup layout
+ setContentView(R.layout.help);
+ String title = getResources().getString(R.string.app_name) + " - " +
+ getResources().getString(R.string.help);
+ setTitle(title);
+
+
+ // Programmatically load text from an asset and place it into the
+ // text view. Note that the text we are loading is ASCII, so we
+ // need to convert it to UTF-16.
+ try {
+ InputStream is = getAssets().open("help.html");
+
+ // We guarantee that the available method returns the total
+ // size of the asset... of course, this does mean that a single
+ // asset can't be more than 2 gigs.
+ int size = is.available();
+
+ // Read the entire asset into a local byte buffer.
+ byte[] buffer = new byte[size];
+ is.read(buffer);
+ is.close();
+
+ // Convert the buffer into a Java string.
+ String text = new String(buffer);
+
+ final String mimeType = "text/html";
+ final String encoding = "utf-8";
+
+ // Finally stick the string into the text view.
+ WebView wv = (WebView)findViewById(R.id.help);
+ wv.loadData(text, mimeType, encoding);
+ } catch (IOException e) {
+ // Should never happen!
+ throw new RuntimeException(e);
+ }
+
+ }
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ if (debug) Log.d(TAG,"onResume()");
+
+ if (CategoryList.isSignedIn() == false) {
+ finish();
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ menu.add(0,CLOSE_HELP_INDEX, 0, R.string.close)
+ .setIcon(android.R.drawable.ic_menu_close_clear_cancel)
+ .setShortcut('0', 'w');
+
+ return super.onCreateOptionsMenu(menu);
+ }
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch(item.getItemId()) {
+ case CLOSE_HELP_INDEX:
+ finish();
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
\ No newline at end of file
diff --git a/src/org/openintents/safe/LogOffActivity.java b/src/org/openintents/safe/LogOffActivity.java
new file mode 100644
index 0000000..f939e58
--- /dev/null
+++ b/src/org/openintents/safe/LogOffActivity.java
@@ -0,0 +1,56 @@
+package org.openintents.safe;
+
+import org.openintents.safe.service.ServiceDispatchImpl;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+
+public class LogOffActivity extends Activity {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.log_off);
+ ImageView icon = (ImageView) findViewById(R.id.logoff_icon);
+ icon.setImageResource(R.drawable.passicon);
+ TextView header = (TextView) findViewById(R.id.logoff_header);
+ String version = getString(R.string.version);
+ String appName = getString(R.string.app_name);
+ String head = appName + " " + version + "\n";
+ header.setText(head);
+ Button logoffButton = (Button) findViewById(R.id.logoff_button);
+ Button gotoPWS = (Button) findViewById(R.id.goto_pws);
+
+
+ logoffButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View arg0) {
+ Intent serviceIntent = new Intent();
+ serviceIntent.setClass(LogOffActivity.this, ServiceDispatchImpl.class );
+ stopService(serviceIntent);
+
+ /*
+ Intent intent = new Intent(LogOffActivity.this, FrontDoor.class);
+ //intent.setClass (LogOffActivity.this, FrontDoor.class );
+ intent.addCategory(Intent.CATEGORY_LAUNCHER);
+ intent.setAction(Intent.ACTION_MAIN);
+ startActivity(intent);
+ */
+ finish();
+ }});
+
+ gotoPWS.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View arg0) {
+ Intent intent = new Intent(LogOffActivity.this, FrontDoor.class);
+ //intent.setClass (LogOffActivity.this, FrontDoor.class );
+ intent.addCategory(Intent.CATEGORY_LAUNCHER);
+ intent.setAction(Intent.ACTION_MAIN);
+ startActivity(intent);
+ finish();
+ }});
+ }
+}
diff --git a/src/org/openintents/safe/PassEdit.java b/src/org/openintents/safe/PassEdit.java
new file mode 100644
index 0000000..c8072de
--- /dev/null
+++ b/src/org/openintents/safe/PassEdit.java
@@ -0,0 +1,331 @@
+/* $Id: PassEdit.java 93 2009-01-05 08:14:22Z isaac.jones $
+ *
+ * Copyright 2007-2008 Steven Osborn
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openintents.safe;
+
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ActivityNotFoundException;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.ClipboardManager;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+/**
+ * PassEdit Activity
+ *
+ * @author Steven Osborn - http://steven.bitsetters.com
+ * @author Randy McEoin
+ */
+public class PassEdit extends Activity {
+
+ public static final int REQUEST_GEN_PASS = 10;
+
+ public static final int SAVE_PASSWORD_INDEX = Menu.FIRST;
+ public static final int DEL_PASSWORD_INDEX = Menu.FIRST + 1;
+ public static final int GEN_PASSWORD_INDEX = Menu.FIRST + 2;
+
+ private EditText descriptionText;
+ private EditText passwordText;
+ private EditText usernameText;
+ private EditText websiteText;
+ private EditText noteText;
+ private Long RowId;
+ private DBHelper dbHelper = null;
+ private CryptoHelper ch;
+ private boolean pass_gen_ret = false;
+
+ private static boolean debug = false;
+ private static String TAG = "PassEdit";
+
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ if (debug) Log.d(TAG,"onCreate()");
+ String title = getResources().getString(R.string.app_name) + " - "
+ + getResources().getString(R.string.edit_entry);
+ setTitle(title);
+
+ ch = new CryptoHelper();
+ ch.setPassword(PassList.getMasterKey());
+
+ if (dbHelper == null) {
+ dbHelper = new DBHelper(this);
+ }
+
+ setContentView(R.layout.pass_edit);
+
+ descriptionText = (EditText) findViewById(R.id.description);
+ passwordText = (EditText) findViewById(R.id.password);
+ usernameText = (EditText) findViewById(R.id.username);
+ noteText = (EditText) findViewById(R.id.note);
+ websiteText = (EditText) findViewById(R.id.website);
+
+ Button confirmButton = (Button) findViewById(R.id.save);
+ Button goButton = (Button) findViewById(R.id.go);
+
+ RowId = icicle != null ? icicle.getLong(PassList.KEY_ID) : null;
+ if (RowId == null) {
+ Bundle extras = getIntent().getExtras();
+ RowId = extras != null ? extras.getLong(PassList.KEY_ID) : null;
+ }
+
+// populateFields();
+
+ goButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View arg0) {
+
+ Toast.makeText(PassEdit.this, R.string.copy_to_clipboard,
+ Toast.LENGTH_SHORT).show();
+
+ ClipboardManager cb = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+ cb.setText(passwordText.getText().toString());
+
+ Intent i = new Intent(Intent.ACTION_VIEW);
+ Uri u = Uri.parse(websiteText.getText().toString());
+ i.setData(u);
+ try {
+ startActivity(i);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(PassEdit.this, R.string.invalid_website,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+
+ confirmButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View arg0) {
+ // Don't allow the user to enter a blank description, we need
+ // something useful to show in the list
+ if (descriptionText.getText().toString().trim().length() == 0) {
+ Toast.makeText(PassEdit.this, R.string.notify_blank_desc,
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ savePassword();
+ }
+ });
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (RowId != null) {
+ outState.putLong(PassList.KEY_ID, RowId);
+ } else {
+ outState.putLong(PassList.KEY_ID, -1);
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ dbHelper.close();
+ dbHelper = null;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ if (debug) Log.d(TAG,"onResume()");
+
+ if (dbHelper == null) {
+ dbHelper = new DBHelper(this);
+ }
+ if (CategoryList.isSignedIn() == false) {
+ saveState();
+ finish();
+ }
+ populateFields();
+ }
+
+ private void saveState() {
+ PassEntry entry = new PassEntry();
+
+ String passwordPlain = passwordText.getText().toString();
+ String notePlain = noteText.getText().toString();
+ String usernamePlain = usernameText.getText().toString();
+ String websitePlain = websiteText.getText().toString();
+ String descPlain = descriptionText.getText().toString();
+
+ try {
+ entry.category = PassList.getCategoryId();
+ entry.description = ch.encrypt(descPlain);
+ entry.username = ch.encrypt(usernamePlain);
+ entry.password = ch.encrypt(passwordPlain);
+ entry.note = ch.encrypt(notePlain);
+ entry.website = ch.encrypt(websitePlain);
+ } catch (CryptoHelperException e) {
+ Log.e(TAG, e.toString());
+ }
+
+ if (RowId == null || RowId == -1) {
+ dbHelper.addPassword(entry);
+ } else {
+ PassEntry storedEntry = dbHelper.fetchPassword (RowId);
+ //update fields that aren't set in the UI:
+ entry.uniqueName = storedEntry.uniqueName;
+ dbHelper.updatePassword(RowId, entry);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ menu.add(0, SAVE_PASSWORD_INDEX, 0, R.string.save).setIcon(
+ android.R.drawable.ic_menu_save).setShortcut('1', 's');
+ menu.add(0, DEL_PASSWORD_INDEX, 0, R.string.password_delete).setIcon(
+ android.R.drawable.ic_menu_delete).setShortcut('3', 'd');
+ menu.add(0, GEN_PASSWORD_INDEX, 0, "Generate").setIcon(
+ android.R.drawable.ic_menu_set_as).setShortcut('4', 'g');
+
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ /**
+ * Save the password entry and finish the activity.
+ */
+ private void savePassword() {
+ saveState();
+ setResult(RESULT_OK);
+ finish();
+ }
+
+ /**
+ * Prompt the user with a dialog asking them if they really want
+ * to delete the password.
+ */
+ public void deletePassword(){
+ Dialog about = new AlertDialog.Builder(this)
+ .setIcon(R.drawable.passicon)
+ .setTitle(R.string.dialog_delete_password_title)
+ .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ deletePassword2();
+ }
+ })
+ .setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ // do nothing
+ }
+ })
+ .setMessage(R.string.dialog_delete_password_msg)
+ .create();
+ about.show();
+ }
+
+ /**
+ * Follow up for the Delete Password dialog. If we have a RowId then
+ * delete the password, otherwise just finish this Activity.
+ */
+ public void deletePassword2(){
+ if ((RowId != null) && (RowId > 0)) {
+ delPassword(RowId);
+ } else {
+ // user specified to delete a new entry
+ // so simply exit out
+ finish();
+ }
+ }
+ /**
+ * Delete the password entry from the database given the row id within the
+ * database.
+ *
+ * @param Id
+ */
+ private void delPassword(long Id) {
+ dbHelper.deletePassword(Id);
+ setResult(RESULT_OK);
+ finish();
+ }
+
+ /**
+ * Handler for when a MenuItem is selected from the Activity.
+ */
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case SAVE_PASSWORD_INDEX:
+ savePassword();
+ break;
+ case DEL_PASSWORD_INDEX:
+ deletePassword();
+ break;
+ case GEN_PASSWORD_INDEX:
+ Intent i = new Intent(getApplicationContext(), PassGen.class);
+ startActivityForResult(i, REQUEST_GEN_PASS);
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ *
+ */
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent i) {
+ super.onActivityResult(requestCode, resultCode, i);
+
+ if (requestCode == REQUEST_GEN_PASS) {
+ if(resultCode == PassGen.CHANGE_ENTRY_RESULT) {
+ String new_pass = i.getStringExtra(PassGen.NEW_PASS_KEY);
+ Log.d(TAG,new_pass);
+ passwordText.setText(new_pass);
+ pass_gen_ret = true;
+ }
+ }
+ }
+
+ /**
+ *
+ */
+ private void populateFields() {
+ if (debug) Log.d(TAG,"populateFields()");
+ if(pass_gen_ret == true){
+ pass_gen_ret = false;
+ return;
+ }
+ if (RowId != null) {
+ PassEntry row = dbHelper.fetchPassword(RowId);
+ if (row.id > -1) {
+ String cryptDesc = row.description;
+ String cryptWebsite = row.website;
+ String cryptUsername = row.username;
+ String cryptPass = row.password;
+ String cryptNote = row.note;
+ try {
+ descriptionText.setText(ch.decrypt(cryptDesc));
+ websiteText.setText(ch.decrypt(cryptWebsite));
+ usernameText.setText(ch.decrypt(cryptUsername));
+ passwordText.setText(ch.decrypt(cryptPass));
+ noteText.setText(ch.decrypt(cryptNote));
+ } catch (CryptoHelperException e) {
+ Log.e(TAG, e.toString());
+ }
+ }
+ }
+ }
+}
diff --git a/src/org/openintents/safe/PassEntry.java b/src/org/openintents/safe/PassEntry.java
new file mode 100644
index 0000000..8b3f7b1
--- /dev/null
+++ b/src/org/openintents/safe/PassEntry.java
@@ -0,0 +1,45 @@
+/* $Id: PassEntry.java 93 2009-01-05 08:14:22Z isaac.jones $
+ *
+ * Copyright 2007-2008 Steven Osborn
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openintents.safe;
+
+import java.util.ArrayList;
+
+/**
+ *
+ * @author Steven Osborn - http://steven.bitsetters.com
+ */
+public class PassEntry extends Object {
+ public String password;
+ public long id;
+ public long category;
+ public String categoryName;
+ public String description;
+ public String username;
+ public String website;
+ public String uniqueName;
+ // public ArrayList packageAccess;
+ public String note;
+ public String plainPassword;
+ public String plainDescription;
+ public String plainUsername;
+ public String plainWebsite;
+ public String plainNote;
+
+ public static boolean checkPackageAccess (ArrayList packageAccess, String packageName) {
+ return (packageAccess.contains(packageName));
+ }
+}
\ No newline at end of file
diff --git a/src/org/openintents/safe/PassGen.java b/src/org/openintents/safe/PassGen.java
new file mode 100644
index 0000000..602ec37
--- /dev/null
+++ b/src/org/openintents/safe/PassGen.java
@@ -0,0 +1,207 @@
+/* $Id: PassGen.java 71 2008-12-28 19:42:48Z peli0101 $
+ *
+ * Copyright 2007-2008 Steven Osborn
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openintents.safe;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.text.ClipboardManager;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnKeyListener;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.view.View.OnFocusChangeListener;
+
+/**
+ * PassGen Activity
+ *
+ * @author Steven Osborn - http://steven.bitsetters.com
+ */
+public class PassGen extends Activity {
+
+ public static final int CHANGE_ENTRY_RESULT = 2;
+ public static final String NEW_PASS_KEY="new_pass";
+
+ EditText pass_view;
+ EditText pass_len;
+ CheckBox pass_upper;
+ CheckBox pass_lower;
+ CheckBox pass_num;
+ CheckBox pass_symbol;
+ String charset = "";
+
+ Button copy_clip;
+ Button copy_entry;
+ Button cancel;
+
+ private final OnClickListener update_click = new OnClickListener() {
+ public void onClick(View v) {
+ genPassword();
+ }
+ };
+ private final OnCheckedChangeListener update_checked = new OnCheckedChangeListener() {
+ public void onCheckedChanged(CompoundButton buttonView,
+ boolean isChecked) {
+ genPassword();
+ }
+ };
+ private final OnKeyListener update_key = new OnKeyListener() {
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ genPassword();
+ return false;
+ }
+
+ };
+ private final OnFocusChangeListener update_focus = new OnFocusChangeListener() {
+ public void onFocusChange(View v, boolean hasFocus) {
+ genPassword();
+ }
+ };
+
+ private final OnClickListener cancel_listener = new OnClickListener() {
+ public void onClick(View v) {
+ finish();
+ }
+ };
+
+ private final OnClickListener copy_clip_listener = new OnClickListener() {
+ public void onClick(View v) {
+ ClipboardManager cb = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+ cb.setText(pass_view.getText().toString());
+ finish();
+ }
+ };
+
+ private final OnClickListener copy_entry_listener = new OnClickListener() {
+ public void onClick(View v) {
+ getIntent().putExtra(PassGen.NEW_PASS_KEY, pass_view.getText().toString());
+ setResult(CHANGE_ENTRY_RESULT, getIntent());
+ finish();
+ }
+ };
+
+
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ setContentView(R.layout.pass_gen);
+
+ String title = getResources().getString(R.string.app_name) + " - Generate Password";
+ setTitle(title);
+
+ pass_view = (EditText) findViewById(R.id.pass_view);
+ pass_len = (EditText) findViewById(R.id.pass_length);
+ pass_upper = (CheckBox) findViewById(R.id.pass_upper);
+ pass_lower = (CheckBox) findViewById(R.id.pass_lower);
+ pass_num = (CheckBox) findViewById(R.id.pass_num);
+ pass_symbol = (CheckBox) findViewById(R.id.pass_symbol);
+
+ pass_view.setOnClickListener(update_click);
+ pass_len.setOnKeyListener(update_key);
+ pass_len.setOnFocusChangeListener(update_focus);
+ pass_upper.setOnCheckedChangeListener(update_checked);
+ pass_lower.setOnCheckedChangeListener(update_checked);
+ pass_num.setOnCheckedChangeListener(update_checked);
+ pass_symbol.setOnCheckedChangeListener(update_checked);
+
+ copy_clip = (Button) findViewById(R.id.copy_clip);
+ copy_entry = (Button) findViewById(R.id.copy_entry);
+ cancel = (Button) findViewById(R.id.cancel);
+
+ copy_clip.setOnClickListener(copy_clip_listener);
+ copy_entry.setOnClickListener(copy_entry_listener);
+ cancel.setOnClickListener(cancel_listener);
+ }
+
+ /**
+ *
+ */
+ protected void genPassword() {
+ charset = "";
+ StringBuilder pass = new StringBuilder();
+ if(pass_upper.isChecked()) {
+ charset += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ }
+ if(pass_lower.isChecked()) {
+ charset += "abcdefghijklmnopqrstuvwxyz";
+ }
+ if(pass_num.isChecked()) {
+ charset += "0123456789";
+ }
+ if(pass_symbol.isChecked()) {
+ charset += "!@#$%^&*";
+ }
+
+ if (charset.length() == 0) {
+ return;
+ }
+ int len=0;
+ try {
+ len = Integer.parseInt(pass_len.getText().toString());
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ }
+
+ SecureRandom generator = null;
+ try {
+ generator = SecureRandom.getInstance("SHA1PRNG");
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+ for(int i=0;i rows;
+
+ /**
+ * Called when the activity is first created.
+ */
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ if (debug) Log.d(TAG,"onCreate()");
+ setContentView(R.layout.pass_list);
+
+ if (dbHelper==null) {
+ dbHelper = new DBHelper(this);
+ }
+ CategoryId = icicle != null ? icicle.getLong(CategoryList.KEY_ID) : null;
+ if (CategoryId == null) {
+ Bundle extras = getIntent().getExtras();
+ CategoryId = extras != null ? extras.getLong(CategoryList.KEY_ID) : null;
+ }
+ if (CategoryId<1) {
+ finish(); // no valid category less than one
+ }
+
+ String categoryName=getCategoryName(CategoryId);
+ String title = getResources().getString(R.string.app_name) + " - " +
+ getResources().getString(R.string.passwords) + " -" +
+ categoryName;
+ setTitle(title);
+
+ fillData();
+
+ final ListView list = getListView();
+ list.setFocusable(true);
+ list.setOnCreateContextMenuListener(this);
+ registerForContextMenu(list);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ // remember which Category we're looking at
+ if (CategoryId != null) {
+ outState.putLong(CategoryList.KEY_ID, CategoryId);
+ } else {
+ outState.putLong(CategoryList.KEY_ID, -1);
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ if (debug) Log.d(TAG,"onPause()");
+ if (dbHelper != null) {
+ dbHelper.close();
+ dbHelper = null;
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ if (debug) Log.d(TAG,"onResume()");
+
+ if (CategoryList.isSignedIn()==false) {
+ finish();
+ }
+ if (dbHelper == null) {
+ dbHelper = new DBHelper(this);
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+
+ if (debug) Log.d(TAG,"onStop()");
+ if (dbHelper != null) {
+ dbHelper.close();
+ dbHelper=null;
+ }
+ }
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view,
+ ContextMenuInfo menuInfo) {
+
+ AdapterView.AdapterContextMenuInfo info;
+ info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+
+ menu.setHeaderTitle(rows.get(info.position).plainDescription);
+ menu.add(0, EDIT_PASSWORD_INDEX, 0, R.string.password_edit)
+ .setIcon(android.R.drawable.ic_menu_edit)
+ .setAlphabeticShortcut('e');
+ menu.add(0, DEL_PASSWORD_INDEX, 0, R.string.password_delete)
+ .setIcon(android.R.drawable.ic_menu_delete)
+ .setAlphabeticShortcut('d');
+ menu.add(0, MOVE_PASSWORD_INDEX, 0, R.string.move)
+ .setIcon(android.R.drawable.ic_menu_more)
+ .setAlphabeticShortcut('m');
+ }
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ onOptionsItemSelected(item);
+ return true;
+ }
+
+ /**
+ * Populates the password ListView
+ */
+ private void fillData() {
+ // initialize crypto so that we can display readable descriptions in
+ // the list view
+ ch = new CryptoHelper();
+ if(masterKey == null) {
+ masterKey = "";
+ }
+ ch.setPassword(masterKey);
+
+ List items = new ArrayList();
+ rows = dbHelper.fetchAllRows(CategoryId);
+
+ for (PassEntry row : rows) {
+ String cryptDesc = row.description;
+ row.plainDescription = "";
+ try {
+ row.plainDescription = ch.decrypt(cryptDesc);
+ } catch (CryptoHelperException e) {
+ Log.e(TAG,e.toString());
+ }
+ }
+ Collections.sort(rows, new Comparator() {
+ public int compare(PassEntry o1, PassEntry o2) {
+ return o1.plainDescription.compareToIgnoreCase(o2.plainDescription);
+ }});
+ for (PassEntry row : rows) {
+ items.add(row.plainDescription);
+ }
+
+ ArrayAdapter entries =
+ new ArrayAdapter(this, android.R.layout.simple_list_item_1, items);
+ setListAdapter(entries);
+
+ }
+
+ @Override
+ public boolean onMenuOpened(int featureId, Menu menu) {
+ MenuItem miDel = menu.findItem(DEL_PASSWORD_INDEX);
+ MenuItem miMove = menu.findItem(MOVE_PASSWORD_INDEX);
+ if (getSelectedItemPosition() > -1) {
+ miDel.setEnabled(true);
+ miMove.setEnabled(true);
+ } else {
+ miDel.setEnabled(false);
+ miMove.setEnabled(false);
+ }
+ return super.onMenuOpened(featureId, menu);
+ }
+
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ menu.add(0, ADD_PASSWORD_INDEX, 0, R.string.password_add)
+ .setIcon(android.R.drawable.ic_menu_add)
+ .setShortcut('2', 'a');
+ menu.add(0, DEL_PASSWORD_INDEX, 0, R.string.password_delete)
+ .setIcon(android.R.drawable.ic_menu_delete)
+ .setShortcut('3', 'd');
+ menu.add(0, MOVE_PASSWORD_INDEX, 0, R.string.move)
+ .setIcon(android.R.drawable.ic_menu_more)
+ .setShortcut('4', 'm');
+
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ static void setMasterKey(String key) {
+ masterKey = key;
+ }
+
+ static String getMasterKey() {
+ return masterKey;
+ }
+
+ static long getCategoryId() {
+ return CategoryId;
+ }
+
+ private void addPassword() {
+ Intent i = new Intent(this, PassEdit.class);
+ startActivityForResult(i,REQUEST_ADD_PASSWORD);
+ }
+ /**
+ * Prompt the user with a dialog asking them if they really want
+ * to delete the password.
+ */
+ public void deletePassword(final int position){
+ Dialog about = new AlertDialog.Builder(this)
+ .setIcon(R.drawable.passicon)
+ .setTitle(R.string.dialog_delete_password_title)
+ .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ deletePassword2(position);
+ }
+ })
+ .setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ // do nothing
+ }
+ })
+ .setMessage(R.string.dialog_delete_password_msg)
+ .create();
+ about.show();
+ }
+
+ /**
+ * Follow up for the Delete Password dialog. If we have a RowId then
+ * delete the password, otherwise just finish this Activity.
+ */
+ public void deletePassword2(int position){
+ try {
+ delPassword(rows.get(position).id);
+ } catch (IndexOutOfBoundsException e) {
+ // This should only happen when there are no
+ // entries to delete.
+ Log.w(TAG,e.toString());
+ }
+ }
+
+ private void delPassword(long Id) {
+ dbHelper.deletePassword(Id);
+ fillData();
+ }
+
+ /**
+ * Prompt the user with Categories to move the specified
+ * password to and then update the password entry accordingly.
+ *
+ * @param passwordId
+ */
+ private void movePassword(final long passwordId) {
+ final HashMap categoryToId=CategoryList.getCategoryToId(dbHelper);
+ String categoryName=getCategoryName(CategoryId);
+ categoryToId.remove(categoryName);
+ Set categories=categoryToId.keySet();
+ final String[] items=(String[])categories.toArray(new String[categories.size()]);
+ Arrays.sort(items, String.CASE_INSENSITIVE_ORDER);
+
+ new AlertDialog.Builder(PassList.this)
+ .setTitle(R.string.move_select)
+ .setItems(items, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+
+ long newCategoryId=categoryToId.get(items[which]);
+ dbHelper.updatePasswordCategory(passwordId, newCategoryId);
+ String result=getString(R.string.moved_to) + " " + items[which];
+ Toast.makeText(PassList.this, result,
+ Toast.LENGTH_LONG).show();
+ fillData();
+ }
+ })
+ .show();
+ }
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
+ int position=-1;
+ if (info==null) {
+ position=getSelectedItemPosition();
+ } else {
+ // used when this is called from a ContextMenu
+ position=info.position;
+ }
+
+ switch(item.getItemId()) {
+ case ADD_PASSWORD_INDEX:
+ addPassword();
+ break;
+ case EDIT_PASSWORD_INDEX:
+ Intent i = new Intent(this, PassEdit.class);
+ i.putExtra(KEY_ID, rows.get(position).id);
+ i.putExtra(KEY_CATEGORY_ID, CategoryId);
+ startActivityForResult(i,REQUEST_EDIT_PASSWORD);
+ break;
+ case DEL_PASSWORD_INDEX:
+ deletePassword(position);
+ break;
+ case MOVE_PASSWORD_INDEX:
+ movePassword(rows.get(position).id);
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ super.onListItemClick(l, v, position, id);
+
+ Intent i = new Intent(this, PassEdit.class);
+ i.putExtra(KEY_ID, rows.get(position).id);
+ i.putExtra(KEY_CATEGORY_ID, CategoryId);
+ startActivityForResult(i,REQUEST_EDIT_PASSWORD);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent i) {
+ super.onActivityResult(requestCode, resultCode, i);
+
+ if (dbHelper == null) {
+ dbHelper = new DBHelper(this);
+ }
+ if (resultCode==RESULT_OK) {
+ fillData();
+ }
+ }
+
+ /**
+ * Retreive the decrypted category name based on the provided id.
+ *
+ * @param Id category id
+ * @return decrypted category name
+ */
+ private String getCategoryName(long Id) {
+ CategoryEntry category=dbHelper.fetchCategory(Id);
+ category.plainName="";
+ if (ch==null) {
+ ch=new CryptoHelper();
+ }
+ ch.setPassword(masterKey);
+ try {
+ category.plainName = ch.decrypt(category.name);
+ } catch (CryptoHelperException e) {
+ Log.e(TAG,e.toString());
+ }
+ return category.plainName;
+ }
+}
diff --git a/src/org/openintents/safe/Preferences.java b/src/org/openintents/safe/Preferences.java
new file mode 100644
index 0000000..5047028
--- /dev/null
+++ b/src/org/openintents/safe/Preferences.java
@@ -0,0 +1,16 @@
+package org.openintents.safe;
+
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+
+public class Preferences extends PreferenceActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.preferences);
+ }
+
+}
diff --git a/src/org/openintents/safe/Restore.java b/src/org/openintents/safe/Restore.java
new file mode 100644
index 0000000..efdcb07
--- /dev/null
+++ b/src/org/openintents/safe/Restore.java
@@ -0,0 +1,292 @@
+/* $Id: Restore.java 71 2008-12-28 19:42:48Z peli0101 $
+ *
+ * Copyright 2008 Randy McEoin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openintents.safe;
+
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class Restore extends Activity {
+
+ private static boolean debug = false;
+ private static final String TAG = "Restore";
+
+ private DBHelper dbHelper=null;
+ private String masterKey="";
+ private String filename=null;
+ private RestoreDataSet restoreDataSet=null;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ if (debug) Log.d(TAG,"onCreate()");
+
+ if (!CategoryList.isSignedIn()) {
+ Intent frontdoor = new Intent(this, FrontDoor.class);
+ startActivity(frontdoor);
+ finish();
+ }
+
+ setContentView(R.layout.restore);
+ String title = getResources().getString(R.string.app_name) + " - " +
+ getResources().getString(R.string.restore);
+ setTitle(title);
+
+ if (filename==null) {
+ filename=CategoryList.BACKUP_FILENAME;
+ }
+ TextView filenameText;
+ filenameText = (TextView) findViewById(R.id.restore_filename);
+ filenameText.setText(filename);
+
+ TextView restoreInfoText;
+ restoreInfoText = (TextView) findViewById(R.id.restore_info);
+
+ EditText passwordText;
+ passwordText = (EditText) findViewById(R.id.restore_password);
+
+ Button restoreButton;
+ restoreButton = (Button) findViewById(R.id.restore_button);
+
+ if (!backupFileExists(filename)) {
+ passwordText.setVisibility(0);
+ restoreButton.setVisibility(0);
+ restoreInfoText.setText(R.string.restore_no_file);
+ return;
+ }
+
+ restoreInfoText.setText(R.string.restore_set_password);
+
+ passwordText.setVisibility(1);
+ restoreButton.setVisibility(1);
+
+ restoreButton.setOnClickListener(new View.OnClickListener() {
+
+ public void onClick(View arg0) {
+ EditText passwordText;
+ passwordText = (EditText) findViewById(R.id.restore_password);
+
+ String masterPassword = passwordText.getText().toString();
+ read(filename, masterPassword);
+ }
+ });
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ if (debug) Log.d(TAG,"onResume()");
+
+ if (!CategoryList.isSignedIn()) {
+ Intent frontdoor = new Intent(this, FrontDoor.class);
+ startActivity(frontdoor);
+ finish();
+ }
+ }
+
+ private boolean backupFileExists(String filename) {
+ FileReader fr;
+ try {
+ fr = new FileReader(filename);
+ fr.close();
+ } catch (FileNotFoundException e) {
+ return false;
+ } catch (IOException e) {
+ return false;
+ }
+ return true;
+ }
+
+ public boolean read(String filename, String masterPassword) {
+ if (debug) Log.d(TAG,"read("+filename+",)");
+
+ FileReader fr;
+ try {
+ fr = new FileReader(filename);
+ } catch (FileNotFoundException e1) {
+ // e1.printStackTrace();
+ Toast.makeText(Restore.this, getString(R.string.restore_unable_to_open)+
+ " "+e1.getLocalizedMessage(),
+ Toast.LENGTH_LONG).show();
+ return false;
+ }
+
+ SAXParserFactory spf = SAXParserFactory.newInstance();
+ try {
+ SAXParser sp = spf.newSAXParser();
+
+ XMLReader xr = sp.getXMLReader();
+
+ RestoreHandler myRestoreHandler = new RestoreHandler();
+ xr.setContentHandler(myRestoreHandler);
+
+ xr.parse(new InputSource(fr));
+
+ restoreDataSet = myRestoreHandler.getParsedData();
+
+ } catch (ParserConfigurationException e) {
+ //e.printStackTrace();
+ Toast.makeText(Restore.this, getString(R.string.restore_unable_to_open)+
+ " "+e.getLocalizedMessage(),
+ Toast.LENGTH_LONG).show();
+ return false;
+ } catch (SAXException e) {
+ //e.printStackTrace();
+ Toast.makeText(Restore.this, getString(R.string.restore_unable_to_open)+
+ " "+e.getLocalizedMessage(),
+ Toast.LENGTH_LONG).show();
+ return false;
+ } catch (IOException e) {
+ //e.printStackTrace();
+ Toast.makeText(Restore.this, getString(R.string.restore_unable_to_open)+
+ " "+e.getLocalizedMessage(),
+ Toast.LENGTH_LONG).show();
+ return false;
+ }
+
+ if (restoreDataSet.getVersion() != Backup.CURRENT_VERSION) {
+ Toast.makeText(Restore.this, getString(R.string.restore_bad_version)+
+ " "+Integer.toString(restoreDataSet.getVersion()),
+ Toast.LENGTH_LONG).show();
+ return false;
+ }
+ CategoryEntry firstCatEntry=restoreDataSet.getCategories().get(0);
+ if (firstCatEntry==null) {
+ Toast.makeText(Restore.this, getString(R.string.restore_error),
+ Toast.LENGTH_LONG).show();
+ return false;
+ }
+ CryptoHelper ch=new CryptoHelper(CryptoHelper.EncryptionStrong);
+ ch.setPassword(masterPassword);
+
+ String masterKeyEncrypted=restoreDataSet.getMasterKeyEncrypted();
+ masterKey="";
+ try {
+ masterKey = ch.decrypt(masterKeyEncrypted);
+ } catch (CryptoHelperException e) {
+ Log.e(TAG,e.toString());
+ }
+ if (ch.getStatus()==false) {
+ Toast.makeText(Restore.this, getString(R.string.restore_decrypt_error),
+ Toast.LENGTH_LONG).show();
+ Animation shake = AnimationUtils
+ .loadAnimation(Restore.this, R.anim.shake);
+ findViewById(R.id.restore_password).startAnimation(shake);
+
+ return false;
+ }
+ ch=new CryptoHelper(CryptoHelper.EncryptionMedium);
+ ch.setPassword(masterKey);
+
+ String firstCategory="";
+ try {
+ firstCategory = ch.decrypt(firstCatEntry.name);
+ } catch (CryptoHelperException e) {
+ Log.e(TAG,e.toString());
+ }
+ if (ch.getStatus() == false) {
+ Toast.makeText(Restore.this, getString(R.string.restore_decrypt_error),
+ Toast.LENGTH_LONG).show();
+ return false;
+ }
+ if (debug) Log.d(TAG,"firstCategory="+firstCategory);
+
+ dbHelper=new DBHelper(Restore.this);
+
+ String msg=getString(R.string.restore_found)+" "+
+ Integer.toString(restoreDataSet.getTotalEntries())+" "+
+ getString(R.string.restore_passwords)+" "+
+ restoreDataSet.getDate()+".\n"+
+ getString(R.string.dialog_restore_database_msg);
+ Dialog confirm = new AlertDialog.Builder(Restore.this)
+ .setIcon(android.R.drawable.ic_menu_manage)
+ .setTitle(R.string.dialog_restore_database_title)
+ .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ restoreDatabase();
+ }
+ })
+ .setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ dbHelper.close();
+ }
+ })
+ .setMessage(msg)
+ .create();
+ confirm.show();
+
+ return true;
+ }
+
+ private void restoreDatabase() {
+ dbHelper.beginTransaction();
+ dbHelper.deleteDatabase();
+
+ dbHelper.storeMasterKey(restoreDataSet.getMasterKeyEncrypted());
+ CategoryList.setMasterKey(masterKey);
+ PassList.setMasterKey(masterKey);
+ for (CategoryEntry category : restoreDataSet.getCategories()) {
+ if (debug) Log.d(TAG,"category="+category.name);
+ dbHelper.addCategory(category);
+ }
+ int totalPasswords=0;
+ for (PassEntry password : restoreDataSet.getPass()) {
+ totalPasswords++;
+ dbHelper.addPassword(password);
+ }
+ dbHelper.commit();
+ dbHelper.close();
+
+ Toast.makeText(Restore.this, getString(R.string.restore_complete)+
+ " "+Integer.toString(totalPasswords),
+ Toast.LENGTH_LONG).show();
+
+ setResult(RESULT_OK);
+ finish();
+
+ /*
+ Message m = new Message();
+ m.what = CategoryList.MSG_FILLDATA;
+ myViewHandler.sendMessage(m);
+ */
+ }
+}
diff --git a/src/org/openintents/safe/RestoreDataSet.java b/src/org/openintents/safe/RestoreDataSet.java
new file mode 100644
index 0000000..2512550
--- /dev/null
+++ b/src/org/openintents/safe/RestoreDataSet.java
@@ -0,0 +1,129 @@
+/* $Id: RestoreDataSet.java 71 2008-12-28 19:42:48Z peli0101 $
+ *
+ * Copyright 2008 Randy McEoin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openintents.safe;
+
+import java.util.ArrayList;
+
+import android.util.Log;
+
+public class RestoreDataSet {
+
+ private static boolean debug = false;
+ private static final String TAG = "RestoreDataSet";
+
+ private int version = 0;
+ private String date = null;
+ private String masterKeyEncrypted = null;
+ private Long currentCategoryId = new Long(0);
+ private CategoryEntry currentCategory = null;
+ private ArrayList categoryEntries = new ArrayList();
+ private PassEntry currentEntry = null;
+ private ArrayList passEntries = new ArrayList();
+ private int totalEntries = 0;
+
+ public int getVersion() {
+ return version;
+ }
+ public void setVersion(int extractedVersion) {
+ version = extractedVersion;
+ }
+ public String getDate() {
+ return date;
+ }
+ public void setDate(String extractedDate) {
+ date = extractedDate;
+ }
+ public String getMasterKeyEncrypted() {
+ return masterKeyEncrypted;
+ }
+ public void setMasterKeyEncrypted(String extractedKey) {
+ masterKeyEncrypted = extractedKey;
+ }
+ public ArrayList getCategories() {
+ return categoryEntries;
+ }
+ public void newCategory(String extractedCategory) {
+ currentCategory = new CategoryEntry();
+ currentCategoryId++;
+ currentCategory.id = currentCategoryId;
+ currentCategory.name = extractedCategory;
+ }
+ public void storyCategory() {
+ if (currentCategory != null) {
+ categoryEntries.add(currentCategory);
+ currentCategory=null;
+ }
+ }
+ public ArrayList getPass() {
+ return passEntries;
+ }
+ public void newEntry() {
+ currentEntry = new PassEntry();
+ currentEntry.category = currentCategoryId;
+ currentEntry.description="";
+ currentEntry.website="";
+ currentEntry.username="";
+ currentEntry.password="";
+ currentEntry.note="";
+ }
+ public void storeEntry() {
+ // only add an entry if we had all the fields
+ if (debug) Log.d(TAG,currentEntry.description+" "+currentEntry.website+" "+
+ currentEntry.username+" "+currentEntry.password+" "+
+ currentEntry.note);
+ if ((currentEntry != null) &&
+ (currentEntry.description!="") &&
+ (currentEntry.website!="") &&
+ (currentEntry.username!="") &&
+ (currentEntry.password!="") &&
+ (currentEntry.note!="")) {
+ passEntries.add(currentEntry);
+ totalEntries++;
+ }
+ currentEntry = null;
+ }
+ public int getTotalEntries() {
+ return totalEntries;
+ }
+ public void setDescription(String extractedDescription) {
+ if (debug) Log.d(TAG,"setDescription("+extractedDescription+")");
+ if (currentEntry != null) {
+ currentEntry.description += extractedDescription;
+ }
+ }
+ public void setWebsite(String extractedWebsite) {
+ if (debug) Log.d(TAG,"setWebsite("+extractedWebsite+")");
+ if (currentEntry != null) {
+ currentEntry.website += extractedWebsite;
+ }
+ }
+ public void setUsername(String extractedUsername) {
+ if (currentEntry != null) {
+ currentEntry.username += extractedUsername;
+ }
+ }
+ public void setPassword(String extractedPassword) {
+ if (currentEntry != null) {
+ currentEntry.password += extractedPassword;
+ }
+ }
+ public void setNote(String extractedNote) {
+ if (currentEntry != null) {
+ currentEntry.note += extractedNote;
+ }
+ }
+}
diff --git a/src/org/openintents/safe/RestoreHandler.java b/src/org/openintents/safe/RestoreHandler.java
new file mode 100644
index 0000000..d13795e
--- /dev/null
+++ b/src/org/openintents/safe/RestoreHandler.java
@@ -0,0 +1,178 @@
+/* $Id: RestoreHandler.java 71 2008-12-28 19:42:48Z peli0101 $
+ *
+ * Copyright 2008 Randy McEoin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openintents.safe;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import android.util.Log;
+
+public class RestoreHandler extends DefaultHandler {
+
+ private static boolean debug = false;
+ private static final String TAG = "Restore";
+
+ // ===========================================================
+ // Fields
+ // ===========================================================
+
+ private boolean in_apws = false;
+ private boolean in_masterkey = false;
+ private boolean in_category = false;
+ private boolean in_entry = false;
+ private boolean in_description = false;
+ private boolean in_website = false;
+ private boolean in_username = false;
+ private boolean in_password = false;
+ private boolean in_note = false;
+
+ private RestoreDataSet myRestoreDataSet = new RestoreDataSet();
+
+ // ===========================================================
+ // Getter & Setter
+ // ===========================================================
+
+ public RestoreDataSet getParsedData() {
+ return this.myRestoreDataSet;
+ }
+
+ // ===========================================================
+ // Methods
+ // ===========================================================
+ @Override
+ public void startDocument() throws SAXException {
+ this.myRestoreDataSet = new RestoreDataSet();
+ }
+
+ @Override
+ public void endDocument() throws SAXException {
+ // Nothing to do
+ }
+
+ /** Called on opening tags like:
+ * <tag>
+ *
+ * Can provide attribute(s) from xml like:
+ * <tag attribute="attributeValue">*/
+ @Override
+ public void startElement(String namespaceURI, String localName,
+ String qName, Attributes atts) throws SAXException {
+
+ if (localName.equals("AndroidPasswordSafe")) {
+ in_apws = true;
+ String attrValue = atts.getValue("version");
+ int version = Integer.parseInt(attrValue);
+
+ String date = atts.getValue("date");
+
+ myRestoreDataSet.setVersion(version);
+ myRestoreDataSet.setDate(date);
+
+ if (debug) Log.d(TAG,"found APWS "+version+" date "+date);
+
+ }else if (in_apws && localName.equals("MasterKey")) {
+ in_masterkey = true;
+
+ if (debug) Log.d(TAG,"found MasterKey");
+
+ }else if (in_apws && localName.equals("Category")) {
+ in_category = true;
+
+ String name = atts.getValue("name");
+ myRestoreDataSet.newCategory(name);
+
+ if (debug) Log.d(TAG,"found Category "+name);
+
+ }else if (in_category && localName.equals("Entry")) {
+ this.in_entry = true;
+
+ myRestoreDataSet.newEntry();
+
+ if (debug) Log.d(TAG,"found Entry");
+
+ }else if (in_entry && localName.equals("Description")) {
+ in_description = true;
+ }else if (in_entry && localName.equals("Website")) {
+ in_website = true;
+ }else if (in_entry && localName.equals("Username")) {
+ in_username = true;
+ }else if (in_entry && localName.equals("Password")) {
+ in_password = true;
+ }else if (in_entry && localName.equals("Note")) {
+ in_note = true;
+ }
+ }
+
+ /** Called on closing tags like:
+ * </tag>
+ */
+ @Override
+ public void endElement(String namespaceURI, String localName, String qName)
+ throws SAXException {
+
+ if (localName.equals("AndroidPasswordSafe")) {
+ in_apws = false;
+ }else if (in_apws && localName.equals("MasterKey")) {
+ in_masterkey = false;
+ }else if (in_apws && localName.equals("Category")) {
+ in_category = false;
+
+ myRestoreDataSet.storyCategory();
+
+ }else if (in_category && localName.equals("Entry")) {
+ in_entry = false;
+
+ myRestoreDataSet.storeEntry();
+
+ }else if (in_entry && localName.equals("Description")) {
+ in_description = false;
+ }else if (in_entry && localName.equals("Website")) {
+ in_website = false;
+ }else if (in_entry && localName.equals("Username")) {
+ in_username = false;
+ }else if (in_entry && localName.equals("Password")) {
+ in_password = false;
+ }else if (in_entry && localName.equals("Note")) {
+ in_note = false;
+ }
+ }
+
+ /** Called on the following structure:
+ * <tag>characters</tag> */
+ @Override
+ public void characters(char ch[], int start, int length) {
+ if (in_masterkey){
+ myRestoreDataSet.setMasterKeyEncrypted(new String(ch, start, length));
+ }
+ if (in_description){
+ myRestoreDataSet.setDescription(new String(ch, start, length));
+ }
+ if (in_website){
+ myRestoreDataSet.setWebsite(new String(ch, start, length));
+ }
+ if (in_username){
+ myRestoreDataSet.setUsername(new String(ch, start, length));
+ }
+ if (in_password){
+ myRestoreDataSet.setPassword(new String(ch, start, length));
+ }
+ if (in_note){
+ myRestoreDataSet.setNote(new String(ch, start, length));
+ }
+ }
+}
diff --git a/src/org/openintents/safe/service/ServiceDispatch.aidl b/src/org/openintents/safe/service/ServiceDispatch.aidl
new file mode 100644
index 0000000..ba06008
--- /dev/null
+++ b/src/org/openintents/safe/service/ServiceDispatch.aidl
@@ -0,0 +1,25 @@
+/* $Id: $
+ *
+ * Copyright 2008 Isaac Potoczny-Jones
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openintents.safe.service;
+
+interface ServiceDispatch {
+ void setPassword (String masterPasswordIn);
+ String getPassword ();
+ String encrypt (String clearText);
+ String decrypt (String cryptoText);
+ void setTimeoutMinutes(int timeoutMinutesIn);
+}
diff --git a/src/org/openintents/safe/service/ServiceDispatchImpl.java b/src/org/openintents/safe/service/ServiceDispatchImpl.java
new file mode 100644
index 0000000..b657c6d
--- /dev/null
+++ b/src/org/openintents/safe/service/ServiceDispatchImpl.java
@@ -0,0 +1,137 @@
+/* $Id: $
+ *
+ * Copyright 2008 Isaac Potoczny-Jones
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openintents.safe.service;
+
+// TODO: Currently the timer MIGHT not actually de-activate the service
+// if there are still clients attached. Should be fixed.
+
+import org.openintents.intents.CryptoIntents;
+import org.openintents.safe.CryptoHelper;
+import org.openintents.safe.CryptoHelperException;
+
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.util.Log;
+import android.os.CountDownTimer;
+
+public class ServiceDispatchImpl extends Service {
+ private CryptoHelper ch;
+ private String masterKey;
+ private CountDownTimer t;
+ private int timeoutMinutes = 5;
+ private long timeoutUntilStop = timeoutMinutes * 60000;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ // Select the interface to return. If your service only implements
+ // a single interface, you can just return it here without checking
+ // the Intent.
+ return (mBinder);
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ Log.d( "ServieDispatchImpl","onCreate" );
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ masterKey = null;
+ ch = null;
+ ServiceNotification.clearNotification(ServiceDispatchImpl.this);
+
+ Intent intent = new Intent(CryptoIntents.ACTION_CRYPTO_LOGGED_OUT);
+ sendBroadcast(intent);
+
+ Log.d( "ADDERSERVICEIMPL","onDestroy" );
+ }
+
+ private void startTimer () {
+ t = new CountDownTimer(timeoutUntilStop, timeoutUntilStop) {
+ public void onTick(long millisUntilFinished) {
+ //doing nothing.
+ }
+
+ public void onFinish() {
+ stopSelf(); // countdown is over, stop the service.
+ }
+ };
+ }
+
+ private void restartTimer () {
+ // must be started with startTimer first.
+ if (t != null) {
+ t.cancel();
+ t.start();
+ }
+ }
+
+ /**
+ * The ServiceDispatch is defined through IDL
+ */
+ private final ServiceDispatch.Stub mBinder = new ServiceDispatch.Stub() {
+ private String TAG = "SERVICEDISPATCH";
+
+ public String encrypt (String clearText) {
+ restartTimer();
+ String cryptoText = null;
+ try {
+ cryptoText = ch.encrypt (clearText);
+ } catch (CryptoHelperException e) {
+ Log.e(TAG, e.toString());
+ }
+ return (cryptoText);
+ }
+
+ public String decrypt (String cryptoText) {
+ restartTimer();
+ String clearText = null;
+ try {
+ clearText = ch.decrypt (cryptoText);
+ } catch (CryptoHelperException e) {
+ Log.e(TAG, e.toString());
+ }
+ return (clearText);
+ }
+
+ public void setPassword (String masterKeyIn){
+ startTimer(); //should be initial timer start
+ ch = new CryptoHelper(CryptoHelper.EncryptionMedium);
+ ch.setPassword(masterKeyIn);
+ masterKey = masterKeyIn;
+
+ ServiceNotification.setNotification(ServiceDispatchImpl.this);
+ }
+
+ public String getPassword() {
+ restartTimer();
+ return masterKey;
+ }
+
+ public void setTimeoutMinutes (int timeoutMinutesIn){
+ timeoutMinutes = timeoutMinutesIn;
+ Log.d(TAG,"set timeout to "+timeoutMinutes);
+ }
+ };
+
+}
diff --git a/src/org/openintents/safe/service/ServiceNotification.java b/src/org/openintents/safe/service/ServiceNotification.java
new file mode 100644
index 0000000..10c1f10
--- /dev/null
+++ b/src/org/openintents/safe/service/ServiceNotification.java
@@ -0,0 +1,70 @@
+package org.openintents.safe.service;
+
+import org.openintents.safe.LogOffActivity;
+import org.openintents.safe.R;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+
+
+public class ServiceNotification {
+
+ private static final int NOTIFICATION_ID = 1;
+
+ /**
+ * @param context
+ * @param force overwrites preferences, if true updates notification regardless the preferences.
+ */
+ /*
+ public static void updateNotification(Context context) {
+ SharedPreferences prefs = PreferenceManager
+ .getDefaultSharedPreferences(context);
+ //if (prefs.getBoolean(PreferenceActivity.PREFS_SHOW_NOTIFICATION, false)) {
+ if () {
+ setNotification(context);
+ } else {
+ clearNotification(context);
+ }
+
+ //} else {
+
+ //}
+
+ }
+ */
+
+ public static void setNotification(Context context) {
+
+ // look up the notification manager service
+ NotificationManager nm = (NotificationManager) context
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+
+ String text = "Master Key Logged in";
+
+ Notification notification = new Notification(
+ R.drawable.passicon, null, System
+ .currentTimeMillis());
+ notification.flags = Notification.FLAG_ONGOING_EVENT;
+
+ Intent intent = new Intent(context, LogOffActivity.class);
+ PendingIntent pi = PendingIntent.getActivity(context, 0,
+ intent, PendingIntent.FLAG_CANCEL_CURRENT);
+ // Set the info for the views that show in the notification
+ // panel.
+ notification.setLatestEventInfo(context, context
+ .getString(R.string.app_name), text, pi);
+
+ nm.notify(NOTIFICATION_ID, notification);
+ }
+
+ public static void clearNotification(Context context) {
+
+ // look up the notification manager service
+ NotificationManager nm = (NotificationManager) context
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.cancel(NOTIFICATION_ID);
+ }
+}
diff --git a/tests/NOTES b/tests/NOTES
new file mode 100644
index 0000000..ac47cde
--- /dev/null
+++ b/tests/NOTES
@@ -0,0 +1,8 @@
+To perform import testing, execute the following one at a time,
+doing an Import each time.
+
+adb push passwordsafe-test1.csv /sdcard/passwordsafe.csv
+adb push passwordsafe-test2.csv /sdcard/passwordsafe.csv
+adb push passwordsafe-test3.csv /sdcard/passwordsafe.csv
+adb push passwordsafe-test4.csv /sdcard/passwordsafe.csv
+
diff --git a/tests/passwordsafe-test1.csv b/tests/passwordsafe-test1.csv
new file mode 100644
index 0000000..23e112a
--- /dev/null
+++ b/tests/passwordsafe-test1.csv
@@ -0,0 +1,78 @@
+"Missing all headers","Description","Website"
+"passcodes","Tkjst kjntry","www.aol.com","as23","flbc",""
+"passcodes","Voickj Mpokil","","33as","368843",""
+"passcodes","HR Connkjction","","vr843","3220moosgh",""
+"passcodes","Work computkjr ","","as067vr42","fljBrfldshflw",""
+"passcodes","USpokpok","","3285vr23","fljldflsjjf3220",""
+"passcodes","VoickjStrkjpokm ","","40445as4807","fljldflsjjf",""
+"passcodes","pokmkjricpokn Wkjst","","40005470477","",""
+"passcodes","kjZ Ppokss","","200000vr2vr0asvr6","3220",""
+"passcodes","Truck kkjy ","","as65d537","",""
+"passcodes","pokT&T Univkjrspokl ","","asdottie","D368843 (000816)",""
+"passcodes","kjbpoky","","asqwertp","fljldflsjjf3220",""
+"passcodes","kjbpoky Ppokymkjnts","","asqwertp","fljldflsjjf7",""
+"passcodes","Hpoklf.com","","asqwertp","fljldflsjjf",""
+"passcodes","PNC bpoknk","","3220Ssn","1013-fljldflsjjf3220",""
+"passcodes","Smpokrt List rkjg.","","44E2-QF00822DQ03qw","7332420-5779",""
+"passcodes","MI-Tvoickjmpokil 678.475.8400","","8324#","3220#",""
+"passcodes","FlkjxDirkjct ","","qwbertdrqwerter","fljldflsjjf368843",""
+"passcodes","pokir Trpokn","","qwbertdrqwerter@mindspring.ertom","fljldflsjjf",""
+"passcodes","Mindspring kjmpokil 2","","qwbertdrqwerter@mindspring.ertom","fljldflsjjf",""
+"passcodes","Ppokp-Ppokrts","","qwbertdrqwerter@mindspring.ertom","368843",""
+"passcodes","Optonlinkj","","qwbertdrqwerter@optonline.ertom","fljldflsjjf",""
+"passcodes","Optonlinkj kjmpokil","","qwbertdrqwerter@optonline.net","fljldflsjjf",""
+"passcodes","Ppokyppokl","paypal.com","qwbertdrqwerter@optonline.net","fljldflsjjf3220",""
+"passcodes","VolumkjCpokrkj","","qwerttivqwtion ertode 2as857","ordghr # 42790",""
+"passcodes","Spywpokrkj doctor","","FF50-0vrqw6-0E23-4ertBB-Fertvr4-qw503-8ert6D-4asasD-qwqwas3-443ert","",""
+"passcodes","Mpoktkjripokl Qupoklity Trpokcking Systkjm","","qwbertdasliertki","Bghttis36",""
+"passcodes","IpTkjpokm sitkj ","","qwbertdasliertki ","fljldflsjjf",""
+"passcodes","pokkjroVpokntix ","","qwbertdasLIertKIR","ghfd2#31",""
+"passcodes","pokkjrovpokntix.com","","qwbertdasLIertKIR","",""
+"passcodes","LivkjLink (WGC)","","qwbertdasliertkr","Kfltigh3220",""
+"passcodes","MI comp mpokin","","qwbertdasliertr","fljldflsjjf",""
+"passcodes","MI computkjr MI035","","qwbertdasliertr","fljldflsjjf",""
+"passcodes","kjpix - Lispok","","Lrqwkes@epix.net","86wflrd",""
+"passcodes","kjmpokil","","Nits56756@mypqwertks.net, fqwrertervr6vr00, heqwps8as606, jeux74vr3as, weqwld6vr0vr5","",""
+"passcodes","Discovkjr","","Pin number","7854",""
+"passcodes","Lpokndwpokrkj Movikj Guidkj Rkjg","","Rert42MT disertount ertode 20%","208980",""
+"passcodes","Documkjnts to go","","Registrqwtion Number: vr08vr236-5vr63",""," Activation Key: D4K0-8A00FADFFM2E"
+"passcodes","Lotus notkjs","","rqwbertdasliert","Bghttis36",""
+"passcodes","UNIskjpokrch","","Rqwbertdasliertk","6xxxxx",""
+"passcodes","Homkj Sidkj Mortgpokgkj","","Rqwbertdasliertki","R368843",""
+"passcodes","Uunkjt","","Rqwbertdasliertki ","Pithidot",""
+"passcodes","MpokWu homkj Lopokns","","rqwbertdasliertki @mindspring.ertom","fljldflsjjf (upighr)",""
+"passcodes","kjPIX kjmpokil","","Rqwbertdasliertki@epix.net","R368843",""
+"passcodes","Homkj Sidkj quck ppoky","","Rqwbertdasliertki@epix.net","fljldflsjjf",""
+"passcodes","pokmpokzon.com","","Rqwbertdasliertki@mindspring.ertom","fljldflsjjf",""
+"passcodes","pokpplkj id","","Rqwbertdasliertki@mindspring.ertom","fljldflsjjf3220",""
+"passcodes","IMDB Wkjb sitkj","WWW.imdb.com","rqwbertdasliertki@mindspring.ertom","fljldflsjjf",""
+"passcodes","Mindspring","","Rqwbertdasliertki@mindspring.ertom","nghpghfflg8",""
+"passcodes","Mindspring kjmpokil 1","","Rqwbertdasliertki@mindspring.ertom","Nghpghfflg8 r368843",""
+"passcodes","PpoklmGkjpokr.com","","rqwbertdasliertki@mindspring.ertom","7DrKflpNt",""
+"passcodes","Tigkjr Dirkjct","tigerdirect.com","rqwbertdasliertki@mindspring.ertom","fljldflsjjf3220",""
+"passcodes","pokDP Ppokystpoktkjmkjnts","","Rqwbertdasliertki@MITEertH","nghpghfflg8",""
+"passcodes","pokkjtnpok","","Rqwbertdasliertkias","3220fljldflsjjf",""
+"passcodes","Onsitkj trpokining","","Ronqwld.g.qwbertdasliertkias206","Kfltigh3220",""
+"passcodes","pokF wkjb sitkj","","Ronqwld.qwbertdasliertki","$fljldflsjjf3200$",""
+"passcodes","401K.com","","Ssn","368843",""
+"passcodes","Fidkjlity wkjb","","Ssn","nghpghfflg8",""
+"test","pokpokpokpokpokpok","","ssss","sssss","khkh"
+"test1","pokpokpokpokpokpok","","ssss","sssss","khkh"
+"passcodes","pokudiblkj.com","","Upier","fljldflsjjf",""
+"passcodes","Frkjkj rkjpublic ","","Upier","Thflbfls",""
+"passcodes","Gkj onlinkj HHGrkjgg","","Upier","fljldflsjjf3220",""
+"passcodes","mp3sugpokr.com","","upier","fljldflsjjf3220",""
+"passcodes","www.bikkjsbl.org","","Upier","",""
+"passcodes","kjntkjrtpokinMkj.com","","upier@mindpring.ertom","fljldflsjjf",""
+"passcodes","Thinkdb","","Upier@mindspring.ertom","fljldflsjjf",""
+"passcodes","Silkybopokrd.com","","qwertp","fljldflsjjf",""
+"passcodes","Discovkjr Crkjdit Chkjck","","qwertpyre","fljldflsjjf3220",""
+"passcodes","Plpoktinum plus businkjss 7436","","qwertpyre","fljldflsjjf11",""
+"passcodes","Profilkj Protkjct","","qwertpyre_as","fljldflsjjf3220",""
+"passcodes","911 VIN","","WP0EB0vras8GSas60377","",""
+"passcodes","Discovkjr meme","","mememe","fljldflsjjf3220",""
+"passcodes","Discovkjr cpokrd","","","vflmpyrgh2-fljldflsjjf3220",""
+"passcodes","Lispok ","","","314-54-9774",""
+"passcodes","Lrpokkkjs33@hotmpokil.com","","","Porschgh ",""
+"passcodes","Skjcurkj Discovkjr #","","","",""
+"More Passcodes","blah blah ","http://asdfokj.blah.com/asdfokjokjqwerjjo/hthhtt","stuff","more","ack"
diff --git a/tests/passwordsafe-test2.csv b/tests/passwordsafe-test2.csv
new file mode 100644
index 0000000..c87d09b
--- /dev/null
+++ b/tests/passwordsafe-test2.csv
@@ -0,0 +1,78 @@
+"Wrong Header Names","Not good","Website","Username","Password","Notes"
+"passcodes","Tkjst kjntry","www.aol.com","as23","flbc",""
+"passcodes","Voickj Mpokil","","33as","368843",""
+"passcodes","HR Connkjction","","vr843","3220moosgh",""
+"passcodes","Work computkjr ","","as067vr42","fljBrfldshflw",""
+"passcodes","USpokpok","","3285vr23","fljldflsjjf3220",""
+"passcodes","VoickjStrkjpokm ","","40445as4807","fljldflsjjf",""
+"passcodes","pokmkjricpokn Wkjst","","40005470477","",""
+"passcodes","kjZ Ppokss","","200000vr2vr0asvr6","3220",""
+"passcodes","Truck kkjy ","","as65d537","",""
+"passcodes","pokT&T Univkjrspokl ","","asdottie","D368843 (000816)",""
+"passcodes","kjbpoky","","asqwertp","fljldflsjjf3220",""
+"passcodes","kjbpoky Ppokymkjnts","","asqwertp","fljldflsjjf7",""
+"passcodes","Hpoklf.com","","asqwertp","fljldflsjjf",""
+"passcodes","PNC bpoknk","","3220Ssn","1013-fljldflsjjf3220",""
+"passcodes","Smpokrt List rkjg.","","44E2-QF00822DQ03qw","7332420-5779",""
+"passcodes","MI-Tvoickjmpokil 678.475.8400","","8324#","3220#",""
+"passcodes","FlkjxDirkjct ","","qwbertdrqwerter","fljldflsjjf368843",""
+"passcodes","pokir Trpokn","","qwbertdrqwerter@mindspring.ertom","fljldflsjjf",""
+"passcodes","Mindspring kjmpokil 2","","qwbertdrqwerter@mindspring.ertom","fljldflsjjf",""
+"passcodes","Ppokp-Ppokrts","","qwbertdrqwerter@mindspring.ertom","368843",""
+"passcodes","Optonlinkj","","qwbertdrqwerter@optonline.ertom","fljldflsjjf",""
+"passcodes","Optonlinkj kjmpokil","","qwbertdrqwerter@optonline.net","fljldflsjjf",""
+"passcodes","Ppokyppokl","paypal.com","qwbertdrqwerter@optonline.net","fljldflsjjf3220",""
+"passcodes","VolumkjCpokrkj","","qwerttivqwtion ertode 2as857","ordghr # 42790",""
+"passcodes","Spywpokrkj doctor","","FF50-0vrqw6-0E23-4ertBB-Fertvr4-qw503-8ert6D-4asasD-qwqwas3-443ert","",""
+"passcodes","Mpoktkjripokl Qupoklity Trpokcking Systkjm","","qwbertdasliertki","Bghttis36",""
+"passcodes","IpTkjpokm sitkj ","","qwbertdasliertki ","fljldflsjjf",""
+"passcodes","pokkjroVpokntix ","","qwbertdasLIertKIR","ghfd2#31",""
+"passcodes","pokkjrovpokntix.com","","qwbertdasLIertKIR","",""
+"passcodes","LivkjLink (WGC)","","qwbertdasliertkr","Kfltigh3220",""
+"passcodes","MI comp mpokin","","qwbertdasliertr","fljldflsjjf",""
+"passcodes","MI computkjr MI035","","qwbertdasliertr","fljldflsjjf",""
+"passcodes","kjpix - Lispok","","Lrqwkes@epix.net","86wflrd",""
+"passcodes","kjmpokil","","Nits56756@mypqwertks.net, fqwrertervr6vr00, heqwps8as606, jeux74vr3as, weqwld6vr0vr5","",""
+"passcodes","Discovkjr","","Pin number","7854",""
+"passcodes","Lpokndwpokrkj Movikj Guidkj Rkjg","","Rert42MT disertount ertode 20%","208980",""
+"passcodes","Documkjnts to go","","Registrqwtion Number: vr08vr236-5vr63",""," Activation Key: D4K0-8A00FADFFM2E"
+"passcodes","Lotus notkjs","","rqwbertdasliert","Bghttis36",""
+"passcodes","UNIskjpokrch","","Rqwbertdasliertk","6xxxxx",""
+"passcodes","Homkj Sidkj Mortgpokgkj","","Rqwbertdasliertki","R368843",""
+"passcodes","Uunkjt","","Rqwbertdasliertki ","Pithidot",""
+"passcodes","MpokWu homkj Lopokns","","rqwbertdasliertki @mindspring.ertom","fljldflsjjf (upighr)",""
+"passcodes","kjPIX kjmpokil","","Rqwbertdasliertki@epix.net","R368843",""
+"passcodes","Homkj Sidkj quck ppoky","","Rqwbertdasliertki@epix.net","fljldflsjjf",""
+"passcodes","pokmpokzon.com","","Rqwbertdasliertki@mindspring.ertom","fljldflsjjf",""
+"passcodes","pokpplkj id","","Rqwbertdasliertki@mindspring.ertom","fljldflsjjf3220",""
+"passcodes","IMDB Wkjb sitkj","WWW.imdb.com","rqwbertdasliertki@mindspring.ertom","fljldflsjjf",""
+"passcodes","Mindspring","","Rqwbertdasliertki@mindspring.ertom","nghpghfflg8",""
+"passcodes","Mindspring kjmpokil 1","","Rqwbertdasliertki@mindspring.ertom","Nghpghfflg8 r368843",""
+"passcodes","PpoklmGkjpokr.com","","rqwbertdasliertki@mindspring.ertom","7DrKflpNt",""
+"passcodes","Tigkjr Dirkjct","tigerdirect.com","rqwbertdasliertki@mindspring.ertom","fljldflsjjf3220",""
+"passcodes","pokDP Ppokystpoktkjmkjnts","","Rqwbertdasliertki@MITEertH","nghpghfflg8",""
+"passcodes","pokkjtnpok","","Rqwbertdasliertkias","3220fljldflsjjf",""
+"passcodes","Onsitkj trpokining","","Ronqwld.g.qwbertdasliertkias206","Kfltigh3220",""
+"passcodes","pokF wkjb sitkj","","Ronqwld.qwbertdasliertki","$fljldflsjjf3200$",""
+"passcodes","401K.com","","Ssn","368843",""
+"passcodes","Fidkjlity wkjb","","Ssn","nghpghfflg8",""
+"test","pokpokpokpokpokpok","","ssss","sssss","khkh"
+"test1","pokpokpokpokpokpok","","ssss","sssss","khkh"
+"passcodes","pokudiblkj.com","","Upier","fljldflsjjf",""
+"passcodes","Frkjkj rkjpublic ","","Upier","Thflbfls",""
+"passcodes","Gkj onlinkj HHGrkjgg","","Upier","fljldflsjjf3220",""
+"passcodes","mp3sugpokr.com","","upier","fljldflsjjf3220",""
+"passcodes","www.bikkjsbl.org","","Upier","",""
+"passcodes","kjntkjrtpokinMkj.com","","upier@mindpring.ertom","fljldflsjjf",""
+"passcodes","Thinkdb","","Upier@mindspring.ertom","fljldflsjjf",""
+"passcodes","Silkybopokrd.com","","qwertp","fljldflsjjf",""
+"passcodes","Discovkjr Crkjdit Chkjck","","qwertpyre","fljldflsjjf3220",""
+"passcodes","Plpoktinum plus businkjss 7436","","qwertpyre","fljldflsjjf11",""
+"passcodes","Profilkj Protkjct","","qwertpyre_as","fljldflsjjf3220",""
+"passcodes","911 VIN","","WP0EB0vras8GSas60377","",""
+"passcodes","Discovkjr meme","","mememe","fljldflsjjf3220",""
+"passcodes","Discovkjr cpokrd","","","vflmpyrgh2-fljldflsjjf3220",""
+"passcodes","Lispok ","","","314-54-9774",""
+"passcodes","Lrpokkkjs33@hotmpokil.com","","","Porschgh ",""
+"passcodes","Skjcurkj Discovkjr #","","","",""
+"More Passcodes","blah blah ","http://asdfokj.blah.com/asdfokjokjqwerjjo/hthhtt","stuff","more","ack"
diff --git a/tests/passwordsafe-test3.csv b/tests/passwordsafe-test3.csv
new file mode 100644
index 0000000..79eae68
--- /dev/null
+++ b/tests/passwordsafe-test3.csv
@@ -0,0 +1,349 @@
+"Category","Description","Website","Username","Password","Notes"
+"Testing","This has more than 256 categories","www.aol.com","as23","flbc",""
+"cat000","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat001","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat002","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat003","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat004","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat005","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat006","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat007","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat009","Voickj Mpokil","","33as","368843",""
+"cat010","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat011","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat012","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat013","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat014","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat015","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat016","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat017","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat019","Voickj Mpokil","","33as","368843",""
+"cat020","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat021","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat022","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat023","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat024","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat025","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat026","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat027","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat029","Voickj Mpokil","","33as","368843",""
+"cat030","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat031","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat032","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat033","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat034","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat035","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat036","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat037","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat039","Voickj Mpokil","","33as","368843",""
+"cat040","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat041","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat042","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat043","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat044","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat045","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat046","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat047","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat049","Voickj Mpokil","","33as","368843",""
+"cat050","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat051","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat052","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat053","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat054","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat055","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat056","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat057","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat059","Voickj Mpokil","","33as","368843",""
+"cat060","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat061","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat062","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat063","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat064","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat065","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat066","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat067","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat069","Voickj Mpokil","","33as","368843",""
+"cat070","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat071","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat072","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat073","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat074","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat075","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat076","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat077","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat079","Voickj Mpokil","","33as","368843",""
+"cat080","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat081","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat082","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat083","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat084","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat085","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat086","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat087","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat089","Voickj Mpokil","","33as","368843",""
+"cat090","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat091","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat092","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat093","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat094","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat095","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat096","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat097","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat099","Voickj Mpokil","","33as","368843",""
+"cat100","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat101","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat102","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat103","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat104","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat105","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat106","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat107","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat109","Voickj Mpokil","","33as","368843",""
+"cat110","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat111","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat112","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat113","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat114","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat115","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat116","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat117","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat119","Voickj Mpokil","","33as","368843",""
+"cat120","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat121","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat122","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat123","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat124","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat125","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat126","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat127","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat129","Voickj Mpokil","","33as","368843",""
+"cat130","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat131","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat132","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat133","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat134","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat135","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat136","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat137","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat139","Voickj Mpokil","","33as","368843",""
+"cat140","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat141","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat142","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat143","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat144","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat145","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat146","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat147","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat149","Voickj Mpokil","","33as","368843",""
+"cat150","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat151","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat152","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat153","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat154","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat155","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat156","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat157","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat159","Voickj Mpokil","","33as","368843",""
+"cat160","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat161","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat162","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat163","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat164","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat165","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat166","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat167","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat169","Voickj Mpokil","","33as","368843",""
+"cat170","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat171","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat172","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat173","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat174","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat175","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat176","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat177","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat179","Voickj Mpokil","","33as","368843",""
+"cat180","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat181","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat182","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat183","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat184","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat185","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat186","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat187","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat189","Voickj Mpokil","","33as","368843",""
+"cat190","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat191","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat192","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat193","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat194","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat195","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat196","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat197","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat199","Voickj Mpokil","","33as","368843",""
+"cat200","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat201","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat202","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat203","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat204","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat205","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat206","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat207","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat209","Voickj Mpokil","","33as","368843",""
+"cat210","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat211","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat212","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat213","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat214","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat215","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat216","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat217","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat219","Voickj Mpokil","","33as","368843",""
+"cat220","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat221","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat222","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat223","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat224","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat225","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat226","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat227","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat229","Voickj Mpokil","","33as","368843",""
+"cat230","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat231","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat232","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat233","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat234","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat235","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat236","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat237","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat239","Voickj Mpokil","","33as","368843",""
+"cat240","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat241","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat242","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat243","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat244","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat245","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat246","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat247","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat249","Voickj Mpokil","","33as","368843",""
+"cat250","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat251","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat252","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat253","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat254","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat255","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat256","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat257","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat259","Voickj Mpokil","","33as","368843",""
+"cat260","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat261","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat262","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat263","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat264","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat265","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat266","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat267","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat269","Voickj Mpokil","","33as","368843",""
+"cat270","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat271","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat272","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat273","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat274","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat275","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat276","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat277","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat279","Voickj Mpokil","","33as","368843",""
+"cat280","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat281","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat282","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat283","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat284","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat285","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat286","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat287","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat289","Voickj Mpokil","","33as","368843",""
+"cat290","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat291","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat292","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat293","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat294","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat295","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat296","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat297","Tkjst kjntry","www.aol.com","as23","flbc",""
+"cat299","Voickj Mpokil","","33as","368843",""
+"cat1","HR Connkjction","","vr843","3220moosgh",""
+"cat1","HR Connkjction","","vr843","3220moosgh",""
+"cat1","HR Connkjction","","vr843","3220moosgh",""
+"cat1","Work computkjr ","","as067vr42","fljBrfldshflw",""
+"cat1","USpokpok","","3285vr23","fljldflsjjf3220",""
+"cat1","VoickjStrkjpokm ","","40445as4807","fljldflsjjf",""
+"cat1","pokmkjricpokn Wkjst","","40005470477","",""
+"cat1","kjZ Ppokss","","200000vr2vr0asvr6","3220",""
+"cat1","Truck kkjy ","","as65d537","",""
+"cat1","pokT&T Univkjrspokl ","","asdottie","D368843 (000816)",""
+"cat1","kjbpoky","","asqwertp","fljldflsjjf3220",""
+"cat1","kjbpoky Ppokymkjnts","","asqwertp","fljldflsjjf7",""
+"cat1","Hpoklf.com","","asqwertp","fljldflsjjf",""
+"cat1","PNC bpoknk","","3220Ssn","1013-fljldflsjjf3220",""
+"cat1","Smpokrt List rkjg.","","44E2-QF00822DQ03qw","7332420-5779",""
+"cat1","MI-Tvoickjmpokil 678.475.8400","","8324#","3220#",""
+"cat1","FlkjxDirkjct ","","qwbertdrqwerter","fljldflsjjf368843",""
+"cat1","pokir Trpokn","","qwbertdrqwerter@mindspring.ertom","fljldflsjjf",""
+"cat1","Mindspring kjmpokil 2","","qwbertdrqwerter@mindspring.ertom","fljldflsjjf",""
+"cat1","Ppokp-Ppokrts","","qwbertdrqwerter@mindspring.ertom","368843",""
+"cat1","Optonlinkj","","qwbertdrqwerter@optonline.ertom","fljldflsjjf",""
+"cat1","Optonlinkj kjmpokil","","qwbertdrqwerter@optonline.net","fljldflsjjf",""
+"cat1","Ppokyppokl","paypal.com","qwbertdrqwerter@optonline.net","fljldflsjjf3220",""
+"cat1","VolumkjCpokrkj","","qwerttivqwtion ertode 2as857","ordghr # 42790",""
+"cat1","Spywpokrkj doctor","","FF50-0vrqw6-0E23-4ertBB-Fertvr4-qw503-8ert6D-4asasD-qwqwas3-443ert","",""
+"cat1","Mpoktkjripokl Qupoklity Trpokcking Systkjm","","qwbertdasliertki","Bghttis36",""
+"cat1","IpTkjpokm sitkj ","","qwbertdasliertki ","fljldflsjjf",""
+"cat1","pokkjroVpokntix ","","qwbertdasLIertKIR","ghfd2#31",""
+"cat1","pokkjrovpokntix.com","","qwbertdasLIertKIR","",""
+"cat1","LivkjLink (WGC)","","qwbertdasliertkr","Kfltigh3220",""
+"cat1","MI comp mpokin","","qwbertdasliertr","fljldflsjjf",""
+"cat1","MI computkjr MI035","","qwbertdasliertr","fljldflsjjf",""
+"cat1","kjpix - Lispok","","Lrqwkes@epix.net","86wflrd",""
+"cat1","kjmpokil","","Nits56756@mypqwertks.net, fqwrertervr6vr00, heqwps8as606, jeux74vr3as, weqwld6vr0vr5","",""
+"cat1","Discovkjr","","Pin number","7854",""
+"cat1","Lpokndwpokrkj Movikj Guidkj Rkjg","","Rert42MT disertount ertode 20%","208980",""
+"cat1","Documkjnts to go","","Registrqwtion Number: vr08vr236-5vr63",""," Activation Key: D4K0-8A00FADFFM2E"
+"cat1","Lotus notkjs","","rqwbertdasliert","Bghttis36",""
+"cat1","UNIskjpokrch","","Rqwbertdasliertk","6xxxxx",""
+"cat1","Homkj Sidkj Mortgpokgkj","","Rqwbertdasliertki","R368843",""
+"cat1","Uunkjt","","Rqwbertdasliertki ","Pithidot",""
+"cat1","MpokWu homkj Lopokns","","rqwbertdasliertki @mindspring.ertom","fljldflsjjf (upighr)",""
+"cat1","kjPIX kjmpokil","","Rqwbertdasliertki@epix.net","R368843",""
+"cat1","Homkj Sidkj quck ppoky","","Rqwbertdasliertki@epix.net","fljldflsjjf",""
+"cat1","pokmpokzon.com","","Rqwbertdasliertki@mindspring.ertom","fljldflsjjf",""
+"cat1","pokpplkj id","","Rqwbertdasliertki@mindspring.ertom","fljldflsjjf3220",""
+"cat1","IMDB Wkjb sitkj","WWW.imdb.com","rqwbertdasliertki@mindspring.ertom","fljldflsjjf",""
+"cat1","Mindspring","","Rqwbertdasliertki@mindspring.ertom","nghpghfflg8",""
+"cat1","Mindspring kjmpokil 1","","Rqwbertdasliertki@mindspring.ertom","Nghpghfflg8 r368843",""
+"cat1","PpoklmGkjpokr.com","","rqwbertdasliertki@mindspring.ertom","7DrKflpNt",""
+"cat1","Tigkjr Dirkjct","tigerdirect.com","rqwbertdasliertki@mindspring.ertom","fljldflsjjf3220",""
+"cat1","pokDP Ppokystpoktkjmkjnts","","Rqwbertdasliertki@MITEertH","nghpghfflg8",""
+"cat1","pokkjtnpok","","Rqwbertdasliertkias","3220fljldflsjjf",""
+"cat1","Onsitkj trpokining","","Ronqwld.g.qwbertdasliertkias206","Kfltigh3220",""
+"cat1","pokF wkjb sitkj","","Ronqwld.qwbertdasliertki","$fljldflsjjf3200$",""
+"cat1","401K.com","","Ssn","368843",""
+"cat1","Fidkjlity wkjb","","Ssn","nghpghfflg8",""
+"test","pokpokpokpokpokpok","","ssss","sssss","khkh"
+"test1","pokpokpokpokpokpok","","ssss","sssss","khkh"
+"cat1","pokudiblkj.com","","Upier","fljldflsjjf",""
+"cat1","Frkjkj rkjpublic ","","Upier","Thflbfls",""
+"cat1","Gkj onlinkj HHGrkjgg","","Upier","fljldflsjjf3220",""
+"cat1","mp3sugpokr.com","","upier","fljldflsjjf3220",""
+"cat1","www.bikkjsbl.org","","Upier","",""
+"cat1","kjntkjrtpokinMkj.com","","upier@mindpring.ertom","fljldflsjjf",""
+"cat1","Thinkdb","","Upier@mindspring.ertom","fljldflsjjf",""
+"cat1","Silkybopokrd.com","","qwertp","fljldflsjjf",""
+"cat1","Discovkjr Crkjdit Chkjck","","qwertpyre","fljldflsjjf3220",""
+"cat1","Plpoktinum plus businkjss 7436","","qwertpyre","fljldflsjjf11",""
+"cat1","Profilkj Protkjct","","qwertpyre_as","fljldflsjjf3220",""
+"cat1","911 VIN","","WP0EB0vras8GSas60377","",""
+"cat1","Discovkjr meme","","mememe","fljldflsjjf3220",""
+"cat1","Discovkjr cpokrd","","","vflmpyrgh2-fljldflsjjf3220",""
+"cat1","Lispok ","","","314-54-9774",""
+"cat1","Lrpokkkjs33@hotmpokil.com","","","Porschgh ",""
+"cat1","Skjcurkj Discovkjr #","","","",""
+"More Passcodes","blah blah ","http://asdfokj.blah.com/asdfokjokjqwerjjo/hthhtt","stuff","more","ack"
diff --git a/tests/passwordsafe-test4.csv b/tests/passwordsafe-test4.csv
new file mode 100644
index 0000000..81fb1a8
--- /dev/null
+++ b/tests/passwordsafe-test4.csv
@@ -0,0 +1,78 @@
+"Category","Description","Website","Username","Password","Notes"
+"passcodes","Should not error out","www.aol.com","as23","flbc",""
+"passcodes","00 Voickj Mpokil","http://www.google.com","33as","368843","When, in the course of human events, it becomes necessary for one people to dissolve the political bonds which have connected them with another, and to assume among the powers of the earth, the separate and equal station to which the laws of nature and of nature's God entitle them, a decent respect to the opinions of mankind requires that they should declare the causes which impel them to the separation."
+"passcodes","HR Connkjction","","vr843","3220moosgh",""
+"passcodes","Work computkjr ","","as067vr42","fljBrfldshflw",""
+"passcodes","USpokpok","","3285vr23","fljldflsjjf3220",""
+"passcodes","VoickjStrkjpokm ","","40445as4807","fljldflsjjf",""
+"passcodes","pokmkjricpokn Wkjst","","40005470477","",""
+"passcodes","kjZ Ppokss","","200000vr2vr0asvr6","3220",""
+"passcodes","Truck kkjy ","","as65d537","",""
+"passcodes","pokT&T Univkjrspokl ","","asdottie","D368843 (000816)",""
+"passcodes","kjbpoky","","asqwertp","fljldflsjjf3220",""
+"passcodes","kjbpoky Ppokymkjnts","","asqwertp","fljldflsjjf7",""
+"passcodes","Hpoklf.com","","asqwertp","fljldflsjjf",""
+"passcodes","PNC bpoknk","","3220Ssn","1013-fljldflsjjf3220",""
+"passcodes","Smpokrt List rkjg.","","44E2-QF00822DQ03qw","7332420-5779",""
+"passcodes","MI-Tvoickjmpokil 678.475.8400","","8324#","3220#",""
+"passcodes","FlkjxDirkjct ","","qwbertdrqwerter","fljldflsjjf368843",""
+"passcodes","pokir Trpokn","","qwbertdrqwerter@mindspring.ertom","fljldflsjjf",""
+"passcodes","Mindspring kjmpokil 2","","qwbertdrqwerter@mindspring.ertom","fljldflsjjf",""
+"passcodes","Ppokp-Ppokrts","","qwbertdrqwerter@mindspring.ertom","368843",""
+"passcodes","Optonlinkj","","qwbertdrqwerter@optonline.ertom","fljldflsjjf",""
+"passcodes","Optonlinkj kjmpokil","","qwbertdrqwerter@optonline.net","fljldflsjjf",""
+"passcodes","Ppokyppokl","paypal.com","qwbertdrqwer@optonline.net","ljldflsjjf3220",""
+"passcodes","VolumkjCpokrkj","","qwerttivqwtion ertode 2as857","ordghr # 42790",""
+"passcodes","Spywpokrkj doctor","","FF50-0vrqw6-0E23-4ertBB-Fertvr4-qw503-8ert6D-4asasD-qwqwas3-443ert","",""
+"passcodes","Mpoktkjripokl Qupoklity Trpokcking Systkjm","","qwbertdasliertki","Bghttis36",""
+"passcodes","IpTkjpokm sitkj ","","qwbertdasliertki ","fljldflsjjf",""
+"passcodes","pokkjroVpokntix ","","qwbertdasLIertKIR","ghfd2#31",""
+"passcodes","pokkjrovpokntix.com","","qwbertdasLIertKIR","",""
+"passcodes","LivkjLink (WGC)","","qwbertdasliertkr","Kfltigh3220",""
+"passcodes","MI comp mpokin","","qwbertdasliertr","fljldflsjjf",""
+"passcodes","MI computkjr MI035","","qwbertdasliertr","fljldflsjjf",""
+"passcodes","kjpix - Lispok","","Lrqwkes@epix.net","86wflrd",""
+"passcodes","kjmpokil","","Nits56756@mypqwertks.net, fqwrertervr6vr00, heqwps8as606, jeux74vr3as, weqwld6vr0vr5","",""
+"passcodes","Discovkjr","","Pin number","7854",""
+"passcodes","Lpokndwpokrkj Movikj Guidkj Rkjg","","Rert42MT disertount ertode 20%","208980",""
+"passcodes","Documkjnts to go","","Registrqwtion Number: vr08vr236-5vr63",""," Activation Key: D4K0-8A00FADFFM2E"
+"passcodes","Lotus notkjs","","rqwbertdasliert","Bghttis36",""
+"passcodes","UNIskjpokrch","","Rqwbertdasliertk","6xxxxx",""
+"passcodes","Homkj Sidkj Mortgpokgkj","","Rqwbertdasliertki","R368843",""
+"passcodes","Uunkjt","","Rqwbertdasliertki ","Pithidot",""
+"passcodes","MpokWu homkj Lopokns","","rqwbertdasliertki @mindspring.ertom","fljldflsjjf (upighr)",""
+"passcodes","kjPIX kjmpokil","","Rqwbertdasliertki@epix.net","R368843",""
+"passcodes","Homkj Sidkj quck ppoky","","Rqwbertdasliertki@epix.net","fljldflsjjf",""
+"passcodes","pokmpokzon.com","","Rqwbertdasliertki@mindspring.ertom","fljldflsjjf",""
+"passcodes","pokpplkj id","","Rqwbertdasliertki@mindspring.ertom","fljldflsjjf3220",""
+"passcodes","IMDB Wkjb sitkj","WWW.imdb.com","rqwbertdasliertki@mindspring.ertom","fljldflsjjf",""
+"passcodes","Mindspring","","Rqwbertdasliertki@mindspring.ertom","nghpghfflg8",""
+"passcodes","Mindspring kjmpokil 1","","Rqwbertdasliertki@mindspring.ertom","Nghpghfflg8 r368843",""
+"passcodes","PpoklmGkjpokr.com","","rqwbertdasliertki@mindspring.ertom","7DrKflpNt",""
+"passcodes","Tigkjr Dirkjct","tigerdirect.com","rqwbertdasliertki@mindspring.ertom","fljldflsjjf3220",""
+"passcodes","pokDP Ppokystpoktkjmkjnts","","Rqwbertdasliertki@MITEertH","nghpghfflg8",""
+"passcodes","pokkjtnpok","","Rqwbertdasliertkias","3220fljldflsjjf",""
+"passcodes","Onsitkj trpokining","","Ronqwld.g.qwbertdasliertkias206","Kfltigh3220",""
+"passcodes","pokF wkjb sitkj","","Ronqwld.qwbertdasliertki","$fljldflsjjf3200$",""
+"passcodes","401K.com","","Ssn","368843",""
+"passcodes","Fidkjlity wkjb","","Ssn","nghpghfflg8",""
+"test","pokpokpokpokpokpok","","ssss","sssss","khkh"
+"test1","pokpokpokpokpokpok","","ssss","sssss","khkh"
+"passcodes","pokudiblkj.com","","Upier","fljldflsjjf",""
+"passcodes","Frkjkj rkjpublic ","","Upier","Thflbfls",""
+"passcodes","Gkj onlinkj HHGrkjgg","","Upier","fljldflsjjf3220",""
+"passcodes","mp3sugpokr.com","","upier","fljldflsjjf3220",""
+"passcodes","www.bikkjsbl.org","","Upier","",""
+"passcodes","kjntkjrtpokinMkj.com","","upier@mindpring.ertom","fljldflsjjf",""
+"passcodes","Thinkdb","","Upier@mindspring.ertom","fljldflsjjf",""
+"passcodes","Silkybopokrd.com","","qwertp","fljldflsjjf",""
+"passcodes","Discovkjr Crkjdit Chkjck","","qwertpyre","fljldflsjjf3220",""
+"passcodes","Plpoktinum plus businkjss 7436","","qwertpyre","fljldflsjjf11",""
+"passcodes","Profilkj Protkjct","","qwertpyre_as","fljldflsjjf3220",""
+"passcodes","911 VIN","","WP0EB0vras8GSas60377","",""
+"passcodes","Discovkjr meme","","mememe","fljldflsjjf3220",""
+"passcodes","Discovkjr cpokrd","","","vflmpyrgh2-fljldflsjjf3220",""
+"passcodes","Lispok ","","","314-54-9774",""
+"passcodes","Lrpokkkjs33@hotmpokil.com","","","Porschgh ",""
+"passcodes","Skjcurkj Discovkjr #","","","",""
+"More Passcodes","blah blah ","http://asdfokj.blah.com/asdfokjokjqwerjjo/hthhtt","stuff","more","ack"
diff --git a/tests/passwordsafe-test5.csv b/tests/passwordsafe-test5.csv
new file mode 100644
index 0000000..e7a0cc5
--- /dev/null
+++ b/tests/passwordsafe-test5.csv
@@ -0,0 +1,372 @@
+"Category","Description","Website","Username","Password","Notes"
+"Personal","dummy title 1","http://www.com","dummy_login","dummy_pwd",""
+"Business","dummy title 2","http://www.com","dummy_login","dummy_pwd",""
+"Business","dummy title 3","http://www.com","dummy_login","dummy_pwd",""
+"Business","dummy title 4","http://www.com","dummy_login","dummy_pwd",""
+"Business","dummy title 5","http://www.com","dummy_login","dummy_pwd",""
+"Business","dummy title 6","http://www.com","dummy_login","dummy_pwd",""
+"Business","dummy title 7","http://www.com","dummy_login","dummy_pwd",""
+"Business","dummy title 8","http://www.com","dummy_login","dummy_pwd",""
+"Business","dummy title 9","http://www.com","dummy_login","dummy_pwd",""
+"Business","dummy title 10","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 11","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 12","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 13","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 14","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 15","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 16","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 17","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 18","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 19","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 20","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 21","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 22","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 23","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 24","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 25","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 26","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 27","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 28","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 29","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 30","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 31","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 32","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 33","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 34","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 35","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 36","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 37","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 38","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 39","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 40","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 41","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 42","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 43","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 44","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 45","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 46","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 47","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 48","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 49","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 50","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 51","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 52","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 53","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 54","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 55","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 56","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 57","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 58","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 59","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 60","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 61","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 62","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 63","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 64","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 65","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 66","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 67","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 68","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 69","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 70","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 71","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 72","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 73","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 74","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 75","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 76","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 77","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 78","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 79","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 80","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 81","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 82","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 83","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 84","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 85","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 86","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 87","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 88","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 89","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 90","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 91","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 92","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 93","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 94","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 95","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 96","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 97","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 98","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 99","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 101","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 102","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 103","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 104","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 105","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 106","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 107","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 108","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 109","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 110","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 111","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 112","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 113","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 114","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 115","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 116","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 117","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 118","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 119","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 120","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 121","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 122","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 123","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 124","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 125","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 126","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 127","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 128","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 129","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 130","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 131","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 132","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 133","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 134","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 135","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 136","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 137","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 138","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 139","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 140","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 141","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 142","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 143","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 144","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 145","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 146","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 147","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 148","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 149","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 150","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 151","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 152","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 153","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 154","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 155","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 156","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 157","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 158","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 159","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 160","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 161","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 162","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 163","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 164","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 165","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 166","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 167","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 168","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 169","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 170","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 171","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 172","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 173","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 174","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 175","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 176","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 177","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 178","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 179","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 180","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 181","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 182","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 183","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 184","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 185","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 186","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 187","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 188","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 189","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 190","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 191","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 192","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 193","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 194","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 195","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 196","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 197","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 198","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 199","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 200","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 201","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 202","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 203","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 204","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 205","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 206","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 207","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 208","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 209","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 210","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 211","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 212","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 213","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 214","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 215","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 216","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 217","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 218","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 219","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 220","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 221","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 222","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 223","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 224","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 225","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 226","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 227","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 228","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 229","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 230","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 231","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 232","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 233","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 234","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 235","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 236","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 237","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 238","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 239","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 240","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 241","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 242","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 243","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 244","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 245","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 246","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 247","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 248","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 249","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 250","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 251","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 252","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 253","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 254","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 255","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 256","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 257","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 258","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 259","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 260","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 261","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 262","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 263","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 264","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 265","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 266","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 267","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 268","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 269","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 270","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 271","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 272","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 273","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 274","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 275","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 276","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 277","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 278","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 279","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 280","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 281","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 282","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 283","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 284","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 285","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 286","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 287","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 288","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 289","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 290","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 291","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 292","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 293","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 294","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 295","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 296","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 297","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 298","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 299","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 300","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 301","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 302","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 303","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 304","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 305","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 306","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 307","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 308","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 309","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 310","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 311","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 312","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 313","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 314","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 315","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 316","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 317","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 318","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 319","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 320","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 321","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 322","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 323","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 324","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 325","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 326","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 327","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 328","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 329","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 330","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 331","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 332","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 333","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 334","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 335","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 336","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 337","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 338","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 339","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 340","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 341","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 342","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 343","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 344","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 345","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 346","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 347","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 348","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 349","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 350","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 351","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 352","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 353","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 354","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 355","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 356","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 357","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 358","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 359","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 360","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 361","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 362","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 363","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 364","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 365","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 366","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 367","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 368","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 369","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 370","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 371","http://www.com","dummy_login","dummy_pwd",""
+"Personal","dummy title 372","http://www.com","dummy_login","dummy_pwd",""
diff --git a/tests/passwordsafe-test6.csv b/tests/passwordsafe-test6.csv
new file mode 100644
index 0000000..74b31fa
--- /dev/null
+++ b/tests/passwordsafe-test6.csv
@@ -0,0 +1,7 @@
+"Category","Description","Website","Username","Password","Notes"
+"Personal","eBay","http://www.ebay.com","elogin","dummy_pwe","This is a simple test file that should work quite simply."
+"Personal","Amazon","http://www.amazon.com","aummy_login","dummy_pwa","Buy all kinds of stuff"
+"Business","Intranet","http://www.myintranet.com","intralogin","ingrapass",""
+"Business","Extranet","http://www.yahoo.com","yummy_login","dummy_pwy",""
+"Business","Payroll","http://www.google.com","gdummy_login","dummy_pwg",""
+"Business","Fin Admin","http://www.google.com","finlog","finpass","This is the financial system with secret beans."
--
2.50.1