From ff97c2f604447a7db67cb303772d0cefd8c10c28 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Thu, 21 May 2015 15:09:25 -0600 Subject: [PATCH 01/14] Move to Gradle project format. Include separate encryption methods. --- .classpath | 7 - .gitignore | 51 +-- .project | 33 -- AndroidManifest.xml | 17 - NOTICE | 190 ---------- README.asciidoc | 14 +- app/.gitignore | 1 + app/build.gradle | 19 + app/src/main/AndroidManifest.xml | 19 + .../securenote/GetPasswordActivity.java | 28 +- .../securenote/SecureNoteActivity.java | 275 ++++++++++++++ .../securenote/crypto/PasswordEncryptor.java | 123 +++++++ .../crypto/RSAHardwareEncryptor.java | 159 ++++++++ .../main/res}/drawable-hdpi/ic_menu_login.png | Bin .../main/res}/drawable-mdpi/ic_menu_login.png | Bin app/src/main/res/layout/get_password.xml | 42 +++ app/src/main/res/layout/secure_note.xml | 66 ++++ app/src/main/res/menu/secure_note.xml | 7 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 1150 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 834 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 1681 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 2835 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 4138 bytes {res => app/src/main/res}/values/strings.xml | 0 build.gradle | 15 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 49896 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 +++++++++ gradlew.bat | 90 +++++ proguard.cfg | 36 -- project.properties | 11 - res/drawable-hdpi/icon.png | Bin 4147 -> 0 bytes res/drawable-ldpi/icon.png | Bin 1723 -> 0 bytes res/drawable-mdpi/icon.png | Bin 2574 -> 0 bytes res/layout/get_password.xml | 22 -- res/layout/secure_note.xml | 26 -- res/menu/secure_note.xml | 6 - settings.gradle | 1 + .../android/securenote/CryptUtil.java | 79 ---- .../securenote/SecureNoteActivity.java | 346 ------------------ 40 files changed, 1003 insertions(+), 850 deletions(-) delete mode 100644 .classpath delete mode 100644 .project delete mode 100644 AndroidManifest.xml delete mode 100644 NOTICE create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/src/main/AndroidManifest.xml rename {src/com/marakana => app/src/main/java/com/example}/android/securenote/GetPasswordActivity.java (81%) create mode 100644 app/src/main/java/com/example/android/securenote/SecureNoteActivity.java create mode 100644 app/src/main/java/com/example/android/securenote/crypto/PasswordEncryptor.java create mode 100644 app/src/main/java/com/example/android/securenote/crypto/RSAHardwareEncryptor.java rename {res => app/src/main/res}/drawable-hdpi/ic_menu_login.png (100%) rename {res => app/src/main/res}/drawable-mdpi/ic_menu_login.png (100%) create mode 100644 app/src/main/res/layout/get_password.xml create mode 100644 app/src/main/res/layout/secure_note.xml create mode 100644 app/src/main/res/menu/secure_note.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename {res => app/src/main/res}/values/strings.xml (100%) create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat delete mode 100644 proguard.cfg delete mode 100644 project.properties delete mode 100644 res/drawable-hdpi/icon.png delete mode 100644 res/drawable-ldpi/icon.png delete mode 100644 res/drawable-mdpi/icon.png delete mode 100644 res/layout/get_password.xml delete mode 100644 res/layout/secure_note.xml delete mode 100644 res/menu/secure_note.xml create mode 100644 settings.gradle delete mode 100644 src/com/marakana/android/securenote/CryptUtil.java delete mode 100644 src/com/marakana/android/securenote/SecureNoteActivity.java diff --git a/.classpath b/.classpath deleted file mode 100644 index 6c635c0..0000000 --- a/.classpath +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/.gitignore b/.gitignore index 0fd5efe..5bfe9e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,44 +1,7 @@ -# Compiled source # -################### -*.com -*.class -*.dll -*.exe -*.o -*.o.d -*.so - -# Packages # -############ -# it's better to unpack these files and commit the raw source -# git has its own built in compression methods -*.7z -*.dmg -*.gz -*.iso -*.rar -*.tar -*.zip - -# Generated code # -################## -obj/* -libs/* -bin/* -gen/* - -# Logs # -###################### -*.log - -# OS generated files # -###################### -.DS_Store* -ehthumbs.db -Icon? -Thumbs.db - -# Eclipse Artifacts # -##################### -.metadata/* - +.gradle +/local.properties +/.idea/ +*.iml +.DS_Store +/build +/captures diff --git a/.project b/.project deleted file mode 100644 index af23d61..0000000 --- a/.project +++ /dev/null @@ -1,33 +0,0 @@ - - - SecureNote - - - - - - 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/AndroidManifest.xml b/AndroidManifest.xml deleted file mode 100644 index 07d73e1..0000000 --- a/AndroidManifest.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/NOTICE b/NOTICE deleted file mode 100644 index 1c6e3c4..0000000 --- a/NOTICE +++ /dev/null @@ -1,190 +0,0 @@ - - Copyright (c) 2005-2012, Marakana Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - - 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. - - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - diff --git a/README.asciidoc b/README.asciidoc index b6cf4fc..d4ece7d 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -1,13 +1,5 @@ -= About This Project += Storage Encryption -This Android Eclipse project demonstrates how [not] to protect data in an Android application. +This project was created to support various http://thenewcircle.com/training/android/[NewCircle Android Training] courses. -This code was developed to support Marakana's Android Training courses. - -For more info, see http://marakana.com/training/android/ - -== Legal - -Please see ++NOTICE++ file in this directory for copyright, license terms, and legal disclaimers. - -Copyright © 2012 Marakana Inc. +This application is intended to demonstrate the use of Java cryptography to encrypt data in Android. diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..10af4f3 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,19 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 22 + buildToolsVersion "22.0.1" + + defaultConfig { + applicationId "com.example.android.securenote" + minSdkVersion 18 + targetSdkVersion 22 + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0800f33 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/com/marakana/android/securenote/GetPasswordActivity.java b/app/src/main/java/com/example/android/securenote/GetPasswordActivity.java similarity index 81% rename from src/com/marakana/android/securenote/GetPasswordActivity.java rename to app/src/main/java/com/example/android/securenote/GetPasswordActivity.java index 4cd1d16..ebfe3d3 100644 --- a/src/com/marakana/android/securenote/GetPasswordActivity.java +++ b/app/src/main/java/com/example/android/securenote/GetPasswordActivity.java @@ -1,5 +1,5 @@ -package com.marakana.android.securenote; +package com.example.android.securenote; import java.security.NoSuchAlgorithmException; @@ -14,23 +14,18 @@ import android.widget.Button; import android.widget.EditText; -public class GetPasswordActivity extends Activity implements OnClickListener, TextWatcher { - private static final String TAG = "GetPasswordActivity"; +public class GetPasswordActivity extends Activity implements + OnClickListener, TextWatcher { + private static final String TAG = GetPasswordActivity.class.getSimpleName(); public static final String VERIFY_PASSWORD_REQUEST_PARAM = "verifyPassword"; - public static final String MIN_PASSWORD_LENGTH_REQUEST_PARAM = "minPasswordLength"; - public static final String PASSWORD_RESPONSE_PARAM = "password"; private EditText password; - private EditText passwordVerification; - private Button ok; - private Button cancel; - private int minPasswordLength; @Override @@ -71,19 +66,8 @@ protected void onPause() { public void onClick(View v) { if (v == this.ok) { Intent reply = new Intent(); - int passwordLength = this.password.getText().length(); - byte[] password = new byte[passwordLength * 2]; - for (int i = 0; i < passwordLength; i++) { - char ch = this.password.getText().charAt(i); - password[i * 2] = (byte)(ch >> 8); - password[i * 2 + 1] = (byte)ch; - } - try { - reply.putExtra(PASSWORD_RESPONSE_PARAM, CryptUtil.getKey(password, true)); - } catch (NoSuchAlgorithmException e) { - Log.wtf(TAG, "Failed to get key for password", e); - super.setResult(RESULT_CANCELED, reply); - } + String password = this.password.getText().toString(); + reply.putExtra(PASSWORD_RESPONSE_PARAM, password); super.setResult(RESULT_OK, reply); } else if (v == this.cancel) { super.setResult(RESULT_CANCELED); diff --git a/app/src/main/java/com/example/android/securenote/SecureNoteActivity.java b/app/src/main/java/com/example/android/securenote/SecureNoteActivity.java new file mode 100644 index 0000000..fbe35ea --- /dev/null +++ b/app/src/main/java/com/example/android/securenote/SecureNoteActivity.java @@ -0,0 +1,275 @@ + +package com.example.android.securenote; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.nio.CharBuffer; +import java.security.Key; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.EditText; +import android.widget.RadioGroup; +import android.widget.TextView; +import android.widget.Toast; + +import com.example.android.securenote.crypto.PasswordEncryptor; +import com.example.android.securenote.crypto.RSAHardwareEncryptor; + +public class SecureNoteActivity extends Activity implements + OnClickListener, TextWatcher { + private static final String TAG = SecureNoteActivity.class.getSimpleName(); + + private static final String PASSWORD_KEY = "password"; + private static final String CHARSET = "UTF-8"; + private static final String FILENAME = "secure.note"; + + /* Password Activity Actions */ + private static final int GET_PASSWORD_FOR_LOAD = 1; + private static final int GET_PASSWORD_FOR_SAVE = 2; + + private EditText noteText; + private TextView resultText; + private RadioGroup encryptionSelect; + private Button loadButton; + private Button saveButton; + + private PasswordEncryptor passwordEncryptor; + private RSAHardwareEncryptor hardwareEncryptor; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.secure_note); + + this.noteText = (EditText) findViewById(R.id.note_text); + this.resultText = (TextView) findViewById(R.id.text_result); + this.encryptionSelect = (RadioGroup) findViewById(R.id.type_select); + this.loadButton = (Button) findViewById(R.id.load_button); + this.saveButton = (Button) findViewById(R.id.save_button); + + this.loadButton.setOnClickListener(this); + this.saveButton.setOnClickListener(this); + this.noteText.addTextChangedListener(this); + + passwordEncryptor = new PasswordEncryptor(); + hardwareEncryptor = new RSAHardwareEncryptor(this); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.secure_note, menu); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + menu.findItem(R.id.delete_button) + .setEnabled(this.isSecureNoteFilePresent()); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.delete_button: + new AlertDialog.Builder(this) + .setMessage(R.string.delete_alert) + .setCancelable(false) + .setPositiveButton(android.R.string.yes, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + deleteSecureNote(); + } + }).setNegativeButton(android.R.string.no, null).show(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + private void getPassword(int requestCode, boolean verifyPasswords) { + Log.d(TAG, "Getting password"); + Intent intent = new Intent(this, GetPasswordActivity.class); + intent.putExtra(GetPasswordActivity.MIN_PASSWORD_LENGTH_REQUEST_PARAM, 6); + intent.putExtra(GetPasswordActivity.VERIFY_PASSWORD_REQUEST_PARAM, verifyPasswords); + + startActivityForResult(intent, requestCode); + } + + private boolean isSecureNoteFilePresent() { + return getFileStreamPath(FILENAME).exists(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (resultCode) { + case RESULT_OK: + String passkey = data.getStringExtra( + GetPasswordActivity.PASSWORD_RESPONSE_PARAM); + switch (requestCode) { + case GET_PASSWORD_FOR_LOAD: + this.loadSecureNote(passkey); + break; + case GET_PASSWORD_FOR_SAVE: + this.saveSecureNote(passkey); + break; + } + break; + case RESULT_CANCELED: + Log.d(TAG, "Canceled result. Ignoring."); + break; + default: + Log.w(TAG, "Unexpected result: " + resultCode); + } + } + + public void onClick(View v) { + int encryptionType = this.encryptionSelect.getCheckedRadioButtonId(); + switch (v.getId()) { + case R.id.load_button: + if (encryptionType == R.id.type_password) { + getPassword(GET_PASSWORD_FOR_LOAD, false); + } else { + loadSecureNote(null); + } + break; + case R.id.save_button: + if (encryptionType == R.id.type_password) { + getPassword(GET_PASSWORD_FOR_SAVE, true); + } else { + saveSecureNote(null); + } + break; + default: + throw new IllegalArgumentException("Invalid Button"); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + this.noteText.getText().clear(); + } + + private void deleteSecureNote() { + Log.d(TAG, "Deleting note"); + if (super.getFileStreamPath(FILENAME).delete()) { + toast(R.string.deleted_note); + Log.d(TAG, "Deleted note"); + } else { + toast(R.string.failed_to_delete); + Log.e(TAG, "Failed to delete note"); + } + } + + private void saveSecureNote(final String passkey) { + Log.d(TAG, "Saving note"); + new AsyncTask() { + + @Override + protected Boolean doInBackground(String... strings) { + try { + OutputStream out = openFileOutput(FILENAME, MODE_PRIVATE); + String note = strings[0]; + if (passkey == null) { + hardwareEncryptor.encryptData(note.getBytes(), out); + } else { + passwordEncryptor.encryptData(passkey, note.getBytes(), out); + } + Log.d(TAG, "Saved note to " + FILENAME); + + return true; + } catch (Exception e) { + Log.e(TAG, "Failed to save note to " + FILENAME, e); + getFileStreamPath(FILENAME).delete(); + return false; + } + } + + @Override + protected void onPostExecute(Boolean result) { + if (result) { + SecureNoteActivity.this.noteText.getText().clear(); + toast(R.string.saved_note); + } else { + toast(R.string.failed_to_save); + } + } + + }.execute(this.noteText.getText().toString()); + } + + private void loadSecureNote(final String passkey) { + Log.d(TAG, "Loading note..."); + new AsyncTask() { + @Override + protected String doInBackground(Void... params) { + try { + InputStream in = openFileInput(FILENAME); + byte[] decrypted; + if (passkey == null) { + decrypted = hardwareEncryptor.decryptData(in); + } else { + decrypted = passwordEncryptor.decryptData(passkey, in); + } + Log.d(TAG, "Loaded note from " + FILENAME); + return new String(decrypted); + } catch (Exception e) { + Log.e(TAG, "Failed to load note from " + FILENAME, e); + return null; + } + } + + @Override + protected void onPostExecute(String result) { + if (result == null) { + toast(R.string.failed_to_load); + } else { + SecureNoteActivity.this.resultText.setText(result); + toast(R.string.loaded_note); + } + } + }.execute(); + } + + private void toast(int resId) { + Toast.makeText(this, resId, Toast.LENGTH_LONG).show(); + } + + public void afterTextChanged(Editable s) { + this.saveButton.setEnabled(true); + } + + @Override + public void beforeTextChanged(CharSequence s, + int start, + int count, + int after) { } + + @Override + public void onTextChanged(CharSequence s, + int start, + int before, + int count) { } +} diff --git a/app/src/main/java/com/example/android/securenote/crypto/PasswordEncryptor.java b/app/src/main/java/com/example/android/securenote/crypto/PasswordEncryptor.java new file mode 100644 index 0000000..18e4555 --- /dev/null +++ b/app/src/main/java/com/example/android/securenote/crypto/PasswordEncryptor.java @@ -0,0 +1,123 @@ +package com.example.android.securenote.crypto; + +import android.util.Base64; +import android.util.Base64OutputStream; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.io.StringReader; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; + +import javax.crypto.Cipher; +import javax.crypto.CipherOutputStream; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +public class PasswordEncryptor { + + private static final String ENCRYPTION_ALGORITHM = "AES/CBC/PKCS5Padding"; + private static final int KEY_LENGTH = 256; + private static final int SALT_LENGTH = KEY_LENGTH / 8; + private static final String DELIMITER = "&"; + + private SecureRandom mSecureRandom; + + public PasswordEncryptor() { + // Do *not* seed secureRandom! Automatically seeded from system entropy. + mSecureRandom = new SecureRandom(); + } + + /** + * Return a cipher text blob of encrypted data, Base64 encoded. + * + * @throws GeneralSecurityException + */ + public void encryptData(String passphrase, byte[] data, OutputStream out) throws GeneralSecurityException, IOException { + Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM); + + byte[] salt = new byte[SALT_LENGTH]; + mSecureRandom.nextBytes(salt); + + byte[] iv = new byte[cipher.getBlockSize()]; + mSecureRandom.nextBytes(iv); + + Key key = generateSecretKey(passphrase.toCharArray(), salt); + cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv)); + + //Pack the result in a cipher text blob + final byte[] encrypted = cipher.doFinal(data); + try { + out.write(Base64.encode(salt, Base64.NO_WRAP)); + out.write(DELIMITER.getBytes()); + out.write(Base64.encode(iv, Base64.NO_WRAP)); + out.write(DELIMITER.getBytes()); + out.write(Base64.encode(encrypted, Base64.NO_WRAP)); + + out.flush(); + } finally { + out.close(); + } + } + + /** + * Return decrypted data from the received cipher text blob. + * + * @throws GeneralSecurityException + * @throws IOException + */ + public byte[] decryptData(String passphrase, InputStream in) throws GeneralSecurityException, IOException { + //Unpack cipherText + String cipherText = readFile(in); + String[] fields = cipherText.split(DELIMITER); + if (fields.length != 3) { + throw new IllegalArgumentException("Not a valid cipher text blob"); + } + + final byte[] salt = Base64.decode(fields[0], Base64.NO_WRAP); + final byte[] iv = Base64.decode(fields[1], Base64.NO_WRAP); + final byte[] encrypted = Base64.decode(fields[2], Base64.NO_WRAP); + + Key key = generateSecretKey(passphrase.toCharArray(), salt); + Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM); + + cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv)); + + return cipher.doFinal(encrypted); + } + + private SecretKey generateSecretKey(char[] passphraseOrPin, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException { + // Number of PBKDF2 hardening rounds to use. Larger values increase + // computation time. You should select a value that causes computation + // to take >100ms. + final int iterations = 1000; + + SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + KeySpec keySpec = new PBEKeySpec(passphraseOrPin, salt, iterations, KEY_LENGTH); + byte[] keyBytes = secretKeyFactory.generateSecret(keySpec).getEncoded(); + return new SecretKeySpec(keyBytes, "AES"); + } + + private String readFile(InputStream in) throws IOException { + InputStreamReader reader = new InputStreamReader(in); + StringBuilder sb = new StringBuilder(); + + char[] inputBuffer = new char[2048]; + int read; + while ((read = reader.read(inputBuffer)) != -1) { + sb.append(inputBuffer, 0, read); + } + + return sb.toString(); + } +} diff --git a/app/src/main/java/com/example/android/securenote/crypto/RSAHardwareEncryptor.java b/app/src/main/java/com/example/android/securenote/crypto/RSAHardwareEncryptor.java new file mode 100644 index 0000000..26c3c7a --- /dev/null +++ b/app/src/main/java/com/example/android/securenote/crypto/RSAHardwareEncryptor.java @@ -0,0 +1,159 @@ +package com.example.android.securenote.crypto; + +import android.content.Context; +import android.content.SharedPreferences; +import android.security.KeyChain; +import android.security.KeyPairGeneratorSpec; +import android.util.Base64; +import android.util.Base64InputStream; +import android.util.Base64OutputStream; +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.UnrecoverableEntryException; +import java.security.cert.CertificateException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Calendar; +import java.util.Date; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.security.auth.x500.X500Principal; + +public class RSAHardwareEncryptor { + private static final String TAG = + RSAHardwareEncryptor.class.getSimpleName(); + private static final String PROVIDER_NAME = "AndroidKeyStore"; + private static final String KEY_ALGORITHM = "RSA"; + private static final String ENCRYPTION_ALGORITHM = "RSA/ECB/PKCS1Padding"; + + private static final String KEY_PUBLIC = "publickey"; + private static final String KEY_ALIAS = "secureKeyAlias"; + + private SharedPreferences mPublicKeyStore; + + public RSAHardwareEncryptor(Context context) { + mPublicKeyStore = context.getSharedPreferences( + "publickey.store", Context.MODE_PRIVATE); + try { + if (!mPublicKeyStore.contains(KEY_PUBLIC)) { + generatePrivateKey(context); + Log.d(TAG, "Generated hardware-bound key"); + } else { + Log.d(TAG, "Hardware key pair already exists"); + } + } catch (Exception e) { + throw new RuntimeException("Unable to generate key material."); + } + } + + public void encryptData(byte[] data, OutputStream out) throws GeneralSecurityException, IOException { + Key key = retrievePublicKey(); + Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, key); + + //Encode output to file + out = new Base64OutputStream(out, Base64.NO_WRAP); + //Encrypt output to encoder + out = new CipherOutputStream(out, cipher); + + try { + out.write(data); + out.flush(); + } finally { + out.close(); + } + } + + public byte[] decryptData(InputStream in) throws GeneralSecurityException, IOException { + Key key = retrievePrivateKey(); + Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, key); + + //Decode input from file + in = new Base64InputStream(in, Base64.NO_WRAP); + //Decrypt input from decoder + in = new CipherInputStream(in, cipher); + + return readFile(in).getBytes(); + } + + public Key retrievePublicKey() throws NoSuchAlgorithmException, InvalidKeySpecException { + String encodedKey = mPublicKeyStore.getString(KEY_PUBLIC, null); + if (encodedKey == null) { + throw new RuntimeException("Expected valid public key!"); + } + + byte[] publicKey = Base64.decode(encodedKey, Base64.NO_WRAP); + return KeyFactory.getInstance(KEY_ALGORITHM) + .generatePublic(new X509EncodedKeySpec(publicKey)); + } + + public Key retrievePrivateKey() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException, UnrecoverableEntryException { + KeyStore ks = KeyStore.getInstance(PROVIDER_NAME); + ks.load(null); + KeyStore.Entry entry = ks.getEntry(KEY_ALIAS, null); + if (!(entry instanceof KeyStore.PrivateKeyEntry)) { + Log.w(TAG, "Not an instance of a PrivateKeyEntry"); + return null; + } + + return ((KeyStore.PrivateKeyEntry) entry).getPrivateKey(); + } + + private void generatePrivateKey(Context context) throws NoSuchProviderException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { + Calendar cal = Calendar.getInstance(); + Date now = cal.getTime(); + cal.add(Calendar.YEAR, 1); + Date end = cal.getTime(); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance(KEY_ALGORITHM, PROVIDER_NAME); + kpg.initialize(new KeyPairGeneratorSpec.Builder(context) + .setAlias(KEY_ALIAS) + .setStartDate(now) + .setEndDate(end) + .setSerialNumber(BigInteger.valueOf(1)) + .setSubject(new X500Principal("CN=" + KEY_ALIAS)) + .build()); + + //Generate and bind the private key to hardware + KeyPair kp = kpg.generateKeyPair(); + + //Persist the public key + PublicKey publicKey = kp.getPublic(); + String encodedKey = Base64.encodeToString(publicKey.getEncoded(), Base64.NO_WRAP); + mPublicKeyStore.edit().putString(KEY_PUBLIC, encodedKey).apply(); + } + + private String readFile(InputStream in) throws IOException { + InputStreamReader reader = new InputStreamReader(in); + StringBuilder sb = new StringBuilder(); + + char[] inputBuffer = new char[2048]; + int read; + while ((read = reader.read(inputBuffer)) != -1) { + sb.append(inputBuffer, 0, read); + } + + return sb.toString(); + } +} diff --git a/res/drawable-hdpi/ic_menu_login.png b/app/src/main/res/drawable-hdpi/ic_menu_login.png similarity index 100% rename from res/drawable-hdpi/ic_menu_login.png rename to app/src/main/res/drawable-hdpi/ic_menu_login.png diff --git a/res/drawable-mdpi/ic_menu_login.png b/app/src/main/res/drawable-mdpi/ic_menu_login.png similarity index 100% rename from res/drawable-mdpi/ic_menu_login.png rename to app/src/main/res/drawable-mdpi/ic_menu_login.png diff --git a/app/src/main/res/layout/get_password.xml b/app/src/main/res/layout/get_password.xml new file mode 100644 index 0000000..155371e --- /dev/null +++ b/app/src/main/res/layout/get_password.xml @@ -0,0 +1,42 @@ + + + + + + + + + +