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..a851be0 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 https://www.protechtraining.com/android-security-pt15256[Protech Android Security Training] course
-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 Android 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..96fdd87
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,36 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+android {
+ compileSdkVersion 31
+
+ defaultConfig {
+ applicationId "com.example.android.securenote"
+ minSdkVersion 23
+ targetSdkVersion 31
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
+ }
+ }
+
+ buildFeatures {
+ viewBinding true
+ }
+
+ packagingOptions {
+ exclude 'META-INF/atomicfu.kotlin_module'
+ }
+}
+repositories {
+ mavenCentral()
+}
+dependencies {
+ implementation 'androidx.appcompat:appcompat:1.3.0'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1-native-mt'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1-native-mt'
+ implementation 'androidx.security:security-crypto:1.1.0-alpha03'
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0c37edf
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/example/android/securenote/GetPasswordDialog.kt b/app/src/main/java/com/example/android/securenote/GetPasswordDialog.kt
new file mode 100644
index 0000000..f5f8023
--- /dev/null
+++ b/app/src/main/java/com/example/android/securenote/GetPasswordDialog.kt
@@ -0,0 +1,148 @@
+package com.example.android.securenote
+
+import android.content.Context
+import android.os.Bundle
+import android.text.Editable
+import android.text.TextWatcher
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.OnClickListener
+import android.view.ViewGroup
+import com.example.android.securenote.databinding.GetPasswordBinding
+
+
+class GetPasswordDialog : androidx.fragment.app.DialogFragment(), OnClickListener, TextWatcher {
+ private var minPasswordLength: Int = 0
+ private var passwordListener: OnPasswordListener? = null
+ private lateinit var getPasswordBinding: GetPasswordBinding
+
+ interface OnPasswordListener {
+ fun onPasswordValid(requestType: Int, password: String)
+
+ fun onPasswordCancel()
+ }
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ try {
+ passwordListener = context as OnPasswordListener
+ } catch (e: ClassCastException) {
+ throw IllegalArgumentException("Must implement OnPasswordListener")
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ dialog?.setTitle(R.string.get_password_label)
+ getPasswordBinding = GetPasswordBinding.inflate(layoutInflater)
+
+ getPasswordBinding.cancelButton.setOnClickListener(this)
+ getPasswordBinding.okButton.setOnClickListener(this)
+
+ return getPasswordBinding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ val args = arguments ?: return
+
+ val verifyPassword = args.getBoolean(VERIFY_PASSWORD_REQUEST_PARAM, true)
+ if (verifyPassword) {
+ getPasswordBinding.okButton.isEnabled = false
+ minPasswordLength = args.getInt(MIN_PASSWORD_LENGTH_REQUEST_PARAM, 0)
+ if (minPasswordLength > 0) {
+ getPasswordBinding.passwordText.hint = super.getString(
+ R.string.password_hint_min_length,
+ minPasswordLength
+ )
+ }
+ getPasswordBinding.passwordText.addTextChangedListener(this)
+ getPasswordBinding.passwordVerificationText.addTextChangedListener(this)
+ } else {
+ getPasswordBinding.passwordVerificationText.visibility = View.GONE
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ getPasswordBinding.passwordText.text.clear()
+ getPasswordBinding.passwordVerificationText.text.clear()
+ Log.d(TAG, "Cleared password fields")
+ }
+
+ override fun onClick(v: View) {
+ when (v.id) {
+ R.id.ok_button -> {
+ val requestType = requireArguments().getInt(REQUEST_PARAM)
+ val password = getPasswordBinding.passwordText.text.toString()
+ passwordListener!!.onPasswordValid(requestType, password)
+ }
+ R.id.cancel_button -> passwordListener!!.onPasswordCancel()
+ else -> throw IllegalArgumentException("Invalid Button")
+ }
+ // the passwords will be cleared during onPause()
+ dismiss()
+ }
+
+ override fun afterTextChanged(s: Editable) {
+ when {
+ getPasswordBinding.passwordText.length() < minPasswordLength -> {
+ Log.d(TAG, "Password too short")
+ getPasswordBinding.okButton.isEnabled = false
+ }
+ getPasswordBinding.passwordText.length() !=
+ getPasswordBinding.passwordVerificationText.length() -> {
+ Log.d(TAG, "Passwords' length differs")
+ getPasswordBinding.okButton.isEnabled = false
+ }
+ else -> {
+ for (i in getPasswordBinding.passwordText.text.indices) {
+ if (getPasswordBinding.passwordText.text[i] !=
+ getPasswordBinding.passwordVerificationText.text[i]
+ ) {
+ Log.d(TAG, "Passwords differ")
+ getPasswordBinding.okButton.isEnabled = false
+ return
+ }
+ }
+ Log.d(TAG, "Passwords are the same")
+ getPasswordBinding.okButton.isEnabled = true
+ }
+ }
+ }
+
+ override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+ // ignored
+ }
+
+ override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+ // ignored
+ }
+
+ companion object {
+ private val TAG = GetPasswordDialog::class.java.simpleName
+
+ const val VERIFY_PASSWORD_REQUEST_PARAM = "verifyPassword"
+ const val MIN_PASSWORD_LENGTH_REQUEST_PARAM = "minPasswordLength"
+ const val REQUEST_PARAM = "requestType"
+
+ fun newInstance(
+ requestType: Int,
+ minPasswordLength: Int,
+ verifyPassword: Boolean
+ ): GetPasswordDialog {
+ val dialog = GetPasswordDialog()
+ val args = Bundle()
+ args.putBoolean(VERIFY_PASSWORD_REQUEST_PARAM, verifyPassword)
+ args.putInt(MIN_PASSWORD_LENGTH_REQUEST_PARAM, minPasswordLength)
+ args.putInt(REQUEST_PARAM, requestType)
+ dialog.arguments = args
+
+ return dialog
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/android/securenote/SecureNoteActivity.kt b/app/src/main/java/com/example/android/securenote/SecureNoteActivity.kt
new file mode 100644
index 0000000..22eda54
--- /dev/null
+++ b/app/src/main/java/com/example/android/securenote/SecureNoteActivity.kt
@@ -0,0 +1,214 @@
+package com.example.android.securenote
+
+import android.content.Context
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+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.Toast
+import androidx.security.crypto.EncryptedFile
+import androidx.security.crypto.MasterKey
+
+import com.example.android.securenote.crypto.PasswordEncryptor
+import com.example.android.securenote.crypto.RSAHardwareEncryptor
+import com.example.android.securenote.databinding.SecureNoteBinding
+import kotlinx.coroutines.*
+import java.io.File
+
+class SecureNoteActivity : androidx.appcompat.app.AppCompatActivity(), OnClickListener, TextWatcher,
+ GetPasswordDialog.OnPasswordListener {
+ private lateinit var hardwareEncryptor: RSAHardwareEncryptor
+
+ private val isSecureNoteFilePresent: Boolean get() = getFileStreamPath(FILENAME).exists()
+ private lateinit var secureNoteBinding: SecureNoteBinding
+
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ secureNoteBinding = SecureNoteBinding.inflate(layoutInflater)
+ setContentView(secureNoteBinding.root)
+
+ secureNoteBinding.loadButton.setOnClickListener(this)
+ secureNoteBinding.saveButton.setOnClickListener(this)
+ secureNoteBinding.noteText.addTextChangedListener(this)
+ secureNoteBinding.noteText.text = null
+
+ hardwareEncryptor = RSAHardwareEncryptor(this)
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.secure_note, menu)
+ return true
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu): Boolean {
+ menu.findItem(R.id.delete_button).isEnabled = this.isSecureNoteFilePresent
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.delete_button -> {
+ androidx.appcompat.app.AlertDialog.Builder(this)
+ .setMessage(R.string.delete_alert)
+ .setCancelable(false)
+ .setPositiveButton(
+ R.string.yes
+ ) { _, _ -> deleteSecureNote() }
+ .setNegativeButton(R.string.no, null)
+ .show()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ private fun getEncryptedFile(autoDelete: Boolean = false): EncryptedFile {
+ val file = File(filesDir, FILENAME)
+ if (autoDelete && file.exists()) {
+ file.delete()
+ }
+
+ return EncryptedFile.Builder(
+ applicationContext.applicationContext,
+ File(filesDir, FILENAME),
+ MasterKey.Builder(applicationContext)
+ .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
+ .build(),
+ EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
+ ).build()
+ }
+
+ private fun getPassword(requestCode: Int, verifyPasswords: Boolean) {
+ Log.d(TAG, "Getting password")
+ val dialog = GetPasswordDialog.newInstance(
+ requestCode,
+ 6, verifyPasswords
+ )
+ dialog.show(supportFragmentManager, GetPasswordDialog::class.java.simpleName)
+ }
+
+ override fun onPasswordValid(requestType: Int, password: String) {
+ when (requestType) {
+ GET_PASSWORD_FOR_LOAD -> this.loadSecureNote(password)
+ GET_PASSWORD_FOR_SAVE -> this.saveSecureNote(password)
+ }
+ }
+
+ override fun onPasswordCancel() {
+ Log.d(TAG, "Canceled result. Ignoring.")
+ }
+
+ override fun onClick(v: View) {
+ val encryptionType = secureNoteBinding.typeSelect.checkedRadioButtonId
+ when (v.id) {
+ R.id.load_button -> if (encryptionType == R.id.type_password) {
+ getPassword(GET_PASSWORD_FOR_LOAD, false)
+ } else {
+ loadSecureNote(null)
+ }
+ R.id.save_button -> if (encryptionType == R.id.type_password) {
+ getPassword(GET_PASSWORD_FOR_SAVE, true)
+ } else {
+ saveSecureNote(null)
+ }
+ else -> throw IllegalArgumentException("Invalid Button")
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ secureNoteBinding.noteText.text.clear()
+ }
+
+ private fun 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 fun saveSecureNote(passkey: String?) {
+ Log.d(TAG, "Saving note")
+ val noteData = secureNoteBinding.noteText.text.toString().toByteArray()
+ CoroutineScope(Job()).launch(Dispatchers.IO) {
+ try {
+ if (passkey == null) {
+ hardwareEncryptor.encryptData(noteData, openFileOutput(FILENAME, Context.MODE_PRIVATE))
+ } else {
+ PasswordEncryptor.encryptData(passkey, noteData,
+ getEncryptedFile(true).openFileOutput())
+ }
+ Log.d(TAG, "Saved note to $FILENAME")
+ withContext(Dispatchers.Main) {
+ secureNoteBinding.noteText.text.clear()
+ toast(R.string.saved_note)
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to save note to $FILENAME", e)
+ getFileStreamPath(FILENAME).delete()
+ withContext(Dispatchers.Main) {
+ toast(R.string.failed_to_save)
+ }
+ }
+ }
+ }
+
+ private fun loadSecureNote(passkey: String?) {
+ Log.d(TAG, "Loading note...")
+ CoroutineScope(Job()).launch(Dispatchers.IO) {
+ try {
+ val decrypted: ByteArray =
+ if (passkey == null) {
+ hardwareEncryptor.decryptData(openFileInput(FILENAME))
+ } else {
+ PasswordEncryptor.decryptData(passkey,
+ getEncryptedFile().openFileInput())
+ }
+ Log.d(TAG, "Loaded note from $FILENAME")
+ withContext(Dispatchers.Main) {
+ secureNoteBinding.textResult.text = String(decrypted)
+ toast(R.string.loaded_note)
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to load note from $FILENAME", e)
+ }
+ }
+ }
+
+ private fun toast(resId: Int) {
+ Toast.makeText(this, resId, Toast.LENGTH_LONG).show()
+ }
+
+ override fun afterTextChanged(s: Editable) {
+ secureNoteBinding.saveButton.isEnabled = s.isNotEmpty()
+ }
+
+ override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+ }
+
+ override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+ }
+
+ companion object {
+ private val TAG = SecureNoteActivity::class.java.simpleName
+
+ private const val FILENAME = "secure.note"
+
+ /* Password Activity Actions */
+ private const val GET_PASSWORD_FOR_LOAD = 1
+ private const val GET_PASSWORD_FOR_SAVE = 2
+ }
+}
diff --git a/app/src/main/java/com/example/android/securenote/crypto/PasswordEncryptor.kt b/app/src/main/java/com/example/android/securenote/crypto/PasswordEncryptor.kt
new file mode 100644
index 0000000..0398fcd
--- /dev/null
+++ b/app/src/main/java/com/example/android/securenote/crypto/PasswordEncryptor.kt
@@ -0,0 +1,114 @@
+package com.example.android.securenote.crypto
+
+import android.util.Base64
+import java.io.IOException
+import java.io.InputStream
+import java.io.InputStreamReader
+import java.io.OutputStream
+import java.security.GeneralSecurityException
+import java.security.NoSuchAlgorithmException
+import java.security.SecureRandom
+import java.security.spec.InvalidKeySpecException
+
+import javax.crypto.Cipher
+import javax.crypto.SecretKey
+import javax.crypto.SecretKeyFactory
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.PBEKeySpec
+import javax.crypto.spec.SecretKeySpec
+
+object PasswordEncryptor {
+ private const val ENCRYPTION_ALGORITHM = "AES/CBC/PKCS5Padding"
+ private const val KEY_LENGTH = 256
+ private const val SALT_LENGTH = KEY_LENGTH / 8
+ private const val DELIMITER = "&"
+
+ // Do *not* seed secureRandom! Automatically seeded from system entropy.
+ private val secureRandom: SecureRandom = SecureRandom()
+
+ /**
+ * Return a cipher text blob of encrypted data, Base64 encoded.
+ *
+ * @throws GeneralSecurityException
+ * @throws IOException
+ */
+ @Throws(GeneralSecurityException::class, IOException::class)
+ fun encryptData(passphrase: String, data: ByteArray, out: OutputStream) {
+ val cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM)
+
+ val salt = ByteArray(SALT_LENGTH)
+ secureRandom.nextBytes(salt)
+
+ val iv = ByteArray(cipher.blockSize)
+ secureRandom.nextBytes(iv)
+
+ val key = generateSecretKey(passphrase.toCharArray(), salt)
+ cipher.init(Cipher.ENCRYPT_MODE, key, IvParameterSpec(iv))
+
+ //Pack the result in a cipher text blob
+ val encrypted = cipher.doFinal(data)
+ out.write(Base64.encode(salt, Base64.NO_WRAP))
+ out.write(DELIMITER.toByteArray())
+ out.write(Base64.encode(iv, Base64.NO_WRAP))
+ out.write(DELIMITER.toByteArray())
+ out.write(Base64.encode(encrypted, Base64.NO_WRAP))
+ out.flush()
+ out.close()
+ }
+
+ /**
+ * Return decrypted data from the received cipher text blob.
+ *
+ * @throws GeneralSecurityException
+ * @throws IOException
+ */
+ @Throws(GeneralSecurityException::class, IOException::class)
+ fun decryptData(passphrase: String, input: InputStream): ByteArray {
+ //Unpack cipherText
+ val cipherText = readFile(input)
+ val fields =
+ cipherText.split(DELIMITER.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+ if (fields.size != 3) {
+ throw IllegalArgumentException("Not a valid cipher text blob")
+ }
+
+ val salt = Base64.decode(fields[0], Base64.NO_WRAP)
+ val iv = Base64.decode(fields[1], Base64.NO_WRAP)
+ val encrypted = Base64.decode(fields[2], Base64.NO_WRAP)
+
+ val key = generateSecretKey(passphrase.toCharArray(), salt)
+ val cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM)
+
+ cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv))
+
+ return cipher.doFinal(encrypted)
+ }
+
+ @Throws(NoSuchAlgorithmException::class, InvalidKeySpecException::class)
+ private fun generateSecretKey(passphraseOrPin: CharArray, salt: ByteArray): SecretKey {
+ // Number of PBKDF2 hardening rounds to use. Larger values increase
+ // computation time. You should select a value that causes computation
+ // to take >100ms.
+ val iterations = 1000
+
+ val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
+ val keySpec = PBEKeySpec(passphraseOrPin, salt, iterations, KEY_LENGTH)
+ val keyBytes = secretKeyFactory.generateSecret(keySpec).encoded
+ return SecretKeySpec(keyBytes, "AES")
+ }
+
+ @Throws(IOException::class)
+ private fun readFile(input: InputStream): String {
+ val reader = InputStreamReader(input)
+ val sb = StringBuilder()
+
+ val inputBuffer = CharArray(2048)
+ var read: Int = reader.read(inputBuffer)
+ while (read != -1) {
+ sb.append(inputBuffer, 0, read)
+ read = reader.read(inputBuffer)
+ }
+
+ return sb.toString()
+ }
+}
diff --git a/app/src/main/java/com/example/android/securenote/crypto/RSAHardwareEncryptor.kt b/app/src/main/java/com/example/android/securenote/crypto/RSAHardwareEncryptor.kt
new file mode 100644
index 0000000..58c2230
--- /dev/null
+++ b/app/src/main/java/com/example/android/securenote/crypto/RSAHardwareEncryptor.kt
@@ -0,0 +1,174 @@
+package com.example.android.securenote.crypto
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.security.keystore.KeyGenParameterSpec
+import android.security.keystore.KeyProperties
+import android.util.Base64
+import android.util.Base64InputStream
+import android.util.Base64OutputStream
+import android.util.Log
+import androidx.security.crypto.EncryptedSharedPreferences
+import androidx.security.crypto.MasterKey
+import java.io.IOException
+import java.io.InputStream
+import java.io.InputStreamReader
+import java.io.OutputStream
+import java.security.*
+import java.security.spec.InvalidKeySpecException
+import java.security.spec.RSAKeyGenParameterSpec
+import java.security.spec.X509EncodedKeySpec
+import javax.crypto.Cipher
+import javax.crypto.CipherInputStream
+import javax.crypto.CipherOutputStream
+
+
+class RSAHardwareEncryptor(context: Context) {
+ var masterKey = MasterKey.Builder(context)
+ .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
+ .build()
+
+ //Persistent location where we will save the public key
+ private val sharedPreferences: SharedPreferences = EncryptedSharedPreferences.create(
+ context,
+ "secret_shared_prefs",
+ masterKey,
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
+ )
+
+ init {
+ try {
+ if (!sharedPreferences.contains(KEY_PUBLIC)) {
+ generatePrivateKey()
+ Log.d(TAG, "Generated hardware-bound key")
+ } else {
+ Log.d(TAG, "Hardware key pair already exists")
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Unable to generate key material.", e)
+ throw RuntimeException("Unable to generate key material.")
+ }
+ }
+
+ /**
+ * Return a cipher text blob of encrypted data, Base64 encoded.
+ *
+ * @throws GeneralSecurityException
+ * @throws IOException
+ */
+ @Throws(GeneralSecurityException::class, IOException::class)
+ fun encryptData(data: ByteArray, outputStream: OutputStream) {
+ val key = retrievePublicKey()
+ val cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM)
+ cipher.init(Cipher.ENCRYPT_MODE, key)
+
+ // Encode output to file
+ var out: OutputStream = Base64OutputStream(outputStream, Base64.NO_WRAP)
+
+ // Encrypt output to encoder
+ out = CipherOutputStream(out, cipher)
+
+ try {
+ out.write(data)
+ out.flush()
+ } finally {
+ out.close()
+ }
+ }
+
+ /**
+ * Return decrypted data from the received cipher text blob.
+ *
+ * @throws GeneralSecurityException
+ * @throws IOException
+ */
+ @Throws(GeneralSecurityException::class, IOException::class)
+ fun decryptData(inputStream: InputStream): ByteArray {
+ val privateKey = retrievePrivateKey()
+
+ val cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM)
+ cipher.init(Cipher.DECRYPT_MODE, privateKey)
+
+ //Decode input from file
+ var input: InputStream = Base64InputStream(inputStream, Base64.NO_WRAP)
+ //Decrypt input from decoder
+ input = CipherInputStream(input, cipher)
+
+ return readFile(input).toByteArray(Charsets.UTF_8)
+ }
+
+ @Throws(NoSuchAlgorithmException::class, InvalidKeySpecException::class)
+ fun retrievePublicKey(): Key {
+ val encodedKey = sharedPreferences.getString(KEY_PUBLIC, null)
+ ?: throw RuntimeException("Expected valid public key!")
+
+ val publicKey = Base64.decode(encodedKey, Base64.NO_WRAP)
+ return KeyFactory.getInstance(KEY_ALGORITHM)
+ .generatePublic(X509EncodedKeySpec(publicKey))
+ }
+
+ private fun retrievePrivateKey(): PrivateKey? {
+ val keyStore = KeyStore.getInstance(PROVIDER_NAME).apply {
+ load(null)
+ }
+
+ val entry = keyStore.getEntry(KEY_ALIAS, null) as KeyStore.PrivateKeyEntry
+
+ return entry.privateKey
+ }
+
+ @Throws(GeneralSecurityException::class)
+ private fun generatePrivateKey() {
+ val keyPairGenerator =
+ KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, PROVIDER_NAME)
+ keyPairGenerator.initialize(
+ KeyGenParameterSpec.Builder(
+ KEY_ALIAS,
+ KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
+ )
+ .setAlgorithmParameterSpec(RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4))
+ .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
+ .setDigests(
+ KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA384,
+ KeyProperties.DIGEST_SHA512
+ )
+ //.setUserAuthenticationRequired(true) <-- requires fingerprint
+ .build()
+ )
+ val keyPair = keyPairGenerator.generateKeyPair()
+
+ // Persist the public key
+ val publicKey = keyPair.public
+ val encodedKey = Base64.encodeToString(publicKey.encoded, Base64.NO_WRAP)
+ sharedPreferences.edit().putString(KEY_PUBLIC, encodedKey).apply()
+ }
+
+ @Throws(IOException::class)
+ private fun readFile(input: InputStream): String {
+ val reader = InputStreamReader(input)
+ val sb = StringBuilder()
+
+ val inputBuffer = CharArray(2048)
+ var read: Int = reader.read(inputBuffer)
+ while (read != -1) {
+ sb.append(inputBuffer, 0, read)
+ read = reader.read(inputBuffer)
+ }
+
+ return sb.toString()
+ }
+
+ companion object {
+ private val TAG = "RSAHardwareEncryptor"
+ private const val PROVIDER_NAME = "AndroidKeyStore"
+ private const val KEY_ALGORITHM = "RSA"
+ private const val ENCRYPTION_ALGORITHM = "RSA/ECB/PKCS1Padding"
+
+ private const val KEY_PUBLIC = "publickey"
+
+ //KeyStore alias for the private key
+ private const val KEY_ALIAS = "secureKeyAlias"
+ }
+}
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/secure_note.xml b/app/src/main/res/layout/secure_note.xml
new file mode 100644
index 0000000..0e96395
--- /dev/null
+++ b/app/src/main/res/layout/secure_note.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/secure_note.xml b/app/src/main/res/menu/secure_note.xml
new file mode 100644
index 0000000..049301f
--- /dev/null
+++ b/app/src/main/res/menu/secure_note.xml
@@ -0,0 +1,7 @@
+
+
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..6395bed
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..1c5dd0a
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..308150a
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..a0c0ccc
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..20666bf
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/res/values/strings.xml b/app/src/main/res/values/strings.xml
similarity index 51%
rename from res/values/strings.xml
rename to app/src/main/res/values/strings.xml
index 60ccb49..7c2541e 100644
--- a/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -4,17 +4,11 @@
Loaded your note.
Failed to save your note.
Saved your note.
- Save your note to apply your new password.
Secure Note
Secure Note
- Unless you close this application, your secure note
- may be available in the background.
- The text you enter here will be stored securely
-
+ Tell me a secret
Load
- Change Password
Save
- Close
OK
Cancel
Enter your password
@@ -22,12 +16,10 @@
Password (min %d chars)
Password verification
Delete
- Are you sure you wish to delete your secure note
- and reset your password?
-
- Deleted your note and reset your password
- Failed to delete your note and reset your passwod
-
- Your secure note has not been saved. Are you sure
- you wish to close and discard your changes?
+ Are you sure you wish to delete?
+ Deleted your note
+ Failed to delete your note
+ Yes
+ No
+
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..055e40f
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,23 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ ext.kotlin_version = '1.5.21'
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:4.2.2'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..5465fec
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,2 @@
+android.enableJetifier=true
+android.useAndroidX=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8c0fb64
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..f5bbfbe
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon Jul 19 13:41:40 PDT 2021
+distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..91a7e26
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..aec9973
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/proguard.cfg b/proguard.cfg
deleted file mode 100644
index 12dd039..0000000
--- a/proguard.cfg
+++ /dev/null
@@ -1,36 +0,0 @@
--optimizationpasses 5
--dontusemixedcaseclassnames
--dontskipnonpubliclibraryclasses
--dontpreverify
--verbose
--optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
-
--keep public class * extends android.app.Activity
--keep public class * extends android.app.Application
--keep public class * extends android.app.Service
--keep public class * extends android.content.BroadcastReceiver
--keep public class * extends android.content.ContentProvider
--keep public class * extends android.app.backup.BackupAgentHelper
--keep public class * extends android.preference.Preference
--keep public class com.android.vending.licensing.ILicensingService
-
--keepclasseswithmembernames class * {
- native ;
-}
-
--keepclasseswithmembernames class * {
- public (android.content.Context, android.util.AttributeSet);
-}
-
--keepclasseswithmembernames class * {
- public (android.content.Context, android.util.AttributeSet, int);
-}
-
--keepclassmembers enum * {
- public static **[] values();
- public static ** valueOf(java.lang.String);
-}
-
--keep class * implements android.os.Parcelable {
- public static final android.os.Parcelable$Creator *;
-}
diff --git a/project.properties b/project.properties
deleted file mode 100644
index f049142..0000000
--- a/project.properties
+++ /dev/null
@@ -1,11 +0,0 @@
-# This file is automatically generated by Android Tools.
-# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
-#
-# This file must be checked in Version Control Systems.
-#
-# To customize properties used by the Ant build system use,
-# "ant.properties", and override values to adapt the script to your
-# project structure.
-
-# Project target.
-target=android-10
diff --git a/res/drawable-hdpi/icon.png b/res/drawable-hdpi/icon.png
deleted file mode 100644
index 8074c4c..0000000
Binary files a/res/drawable-hdpi/icon.png and /dev/null differ
diff --git a/res/drawable-ldpi/icon.png b/res/drawable-ldpi/icon.png
deleted file mode 100644
index 1095584..0000000
Binary files a/res/drawable-ldpi/icon.png and /dev/null differ
diff --git a/res/drawable-mdpi/icon.png b/res/drawable-mdpi/icon.png
deleted file mode 100644
index a07c69f..0000000
Binary files a/res/drawable-mdpi/icon.png and /dev/null differ
diff --git a/res/layout/get_password.xml b/res/layout/get_password.xml
deleted file mode 100644
index cf3c796..0000000
--- a/res/layout/get_password.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/res/layout/secure_note.xml b/res/layout/secure_note.xml
deleted file mode 100644
index 37e8883..0000000
--- a/res/layout/secure_note.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/res/menu/secure_note.xml b/res/menu/secure_note.xml
deleted file mode 100644
index 5806171..0000000
--- a/res/menu/secure_note.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/src/com/marakana/android/securenote/CryptUtil.java b/src/com/marakana/android/securenote/CryptUtil.java
deleted file mode 100644
index 8bf7c8b..0000000
--- a/src/com/marakana/android/securenote/CryptUtil.java
+++ /dev/null
@@ -1,79 +0,0 @@
-
-package com.marakana.android.securenote;
-
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.Key;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.security.spec.InvalidParameterSpecException;
-import java.util.Arrays;
-
-import javax.crypto.Cipher;
-import javax.crypto.KeyGenerator;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-public class CryptUtil {
-
- public static final int IV_LENGTH = 16;
-
- private static final String ENCRYPTION_ALGORITHM = "AES/CBC/PKCS5Padding";
-
- private static final String KEY_ALGORITHM = "AES";
-
- private static final int KEY_SIZE = 256;
-
- public static Key getKey(byte[] secret) throws NoSuchAlgorithmException {
- return getKey(secret, false);
- }
-
- public static Key getKey(byte[] secret, boolean wipeSecret) throws NoSuchAlgorithmException {
- // generate an encryption/decryption key from random data seeded with
- // our secret (i.e. password)
- SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
- secureRandom.setSeed(secret);
- KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
- keyGenerator.init(KEY_SIZE, secureRandom);
- Key key = new SecretKeySpec(keyGenerator.generateKey().getEncoded(), KEY_ALGORITHM);
- if (wipeSecret) {
- Arrays.fill(secret, (byte)0);
- }
- return key;
- }
-
- public static Cipher getEncryptCipher(Key key) throws NoSuchAlgorithmException,
- NoSuchPaddingException, InvalidKeyException {
- Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM);
- cipher.init(Cipher.ENCRYPT_MODE, key);
- return cipher;
- }
-
- public static Cipher getDecryptCipher(Key key, byte[] iv) throws NoSuchAlgorithmException,
- NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
- Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM);
- cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
- return cipher;
- }
-
- public static byte[] getIv(Cipher cipher) throws InvalidParameterSpecException {
- return cipher.getParameters().getParameterSpec(IvParameterSpec.class).getIV();
- }
-
- public static byte[] getIv(InputStream in) throws IOException {
- byte[] iv = new byte[IV_LENGTH];
- for (int i = 0; i < iv.length;) {
- int nRead = in.read(iv, i, iv.length - i);
- if (nRead == -1) {
- throw new EOFException("Unexpected EOF");
- } else {
- i += nRead;
- }
- }
- return iv;
- }
-}
diff --git a/src/com/marakana/android/securenote/GetPasswordActivity.java b/src/com/marakana/android/securenote/GetPasswordActivity.java
deleted file mode 100644
index 4cd1d16..0000000
--- a/src/com/marakana/android/securenote/GetPasswordActivity.java
+++ /dev/null
@@ -1,123 +0,0 @@
-
-package com.marakana.android.securenote;
-
-import java.security.NoSuchAlgorithmException;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-import android.text.Editable;
-import android.text.TextWatcher;
-import android.util.Log;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.Button;
-import android.widget.EditText;
-
-public class GetPasswordActivity extends Activity implements OnClickListener, TextWatcher {
- private static final String TAG = "GetPasswordActivity";
-
- 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
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- super.setContentView(R.layout.get_password);
- this.password = (EditText)super.findViewById(R.id.password_text);
- this.passwordVerification = (EditText)super.findViewById(R.id.password_verification_text);
- this.ok = (Button)super.findViewById(R.id.ok_button);
- this.cancel = (Button)super.findViewById(R.id.cancel_button);
- this.ok.setOnClickListener(this);
- this.cancel.setOnClickListener(this);
-
- Intent request = super.getIntent();
- boolean verifyPassword = request.getBooleanExtra(VERIFY_PASSWORD_REQUEST_PARAM, true);
- if (verifyPassword) {
- this.ok.setEnabled(false);
- this.minPasswordLength = request.getIntExtra(MIN_PASSWORD_LENGTH_REQUEST_PARAM, 0);
- if (minPasswordLength > 0) {
- this.password.setHint(super.getString(R.string.password_hint_min_length,
- this.minPasswordLength));
- }
- this.password.addTextChangedListener(this);
- this.passwordVerification.addTextChangedListener(this);
- } else {
- this.passwordVerification.setVisibility(View.GONE);
- }
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- this.password.getText().clear();
- this.passwordVerification.getText().clear();
- Log.d(TAG, "Cleared password fields");
- }
-
- 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);
- }
- super.setResult(RESULT_OK, reply);
- } else if (v == this.cancel) {
- super.setResult(RESULT_CANCELED);
- }
- // the passwords will be cleared during onPause()
- super.finish();
- }
-
- public void afterTextChanged(Editable s) {
- if (this.password.length() < this.minPasswordLength) {
- Log.d(TAG, "Password too short");
- this.ok.setEnabled(false);
- } else if (this.password.length() != this.passwordVerification.length()) {
- Log.d(TAG, "Passwords' length differs");
- this.ok.setEnabled(false);
- } else {
- for (int i = 0; i < this.password.getText().length(); i++) {
- if (this.password.getText().charAt(i) != this.passwordVerification.getText()
- .charAt(i)) {
- Log.d(TAG, "Passwords differ");
- this.ok.setEnabled(false);
- return;
- }
- }
- Log.d(TAG, "Passwords are the same");
- this.ok.setEnabled(true);
- }
- }
-
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- // ignored
- }
-
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- // ignored
- }
-}
diff --git a/src/com/marakana/android/securenote/SecureNoteActivity.java b/src/com/marakana/android/securenote/SecureNoteActivity.java
deleted file mode 100644
index 87300ec..0000000
--- a/src/com/marakana/android/securenote/SecureNoteActivity.java
+++ /dev/null
@@ -1,346 +0,0 @@
-
-package com.marakana.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.Toast;
-
-public class SecureNoteActivity extends Activity implements OnClickListener, TextWatcher {
- private static final String PASSWORD_KEY = "password";
-
- private static final String CHARSET = "UTF-8";
-
- private static final String FILENAME = "secure.note";
-
- private static final String TAG = "SecureNoteActivity";
-
- private static final int GET_PASSWORD_FOR_LOAD = 1;
-
- private static final int GET_PASSWORD_FOR_SAVE = 2;
-
- private static final int CHANGE_PASSWORD = 3;
-
- private EditText noteText;
-
- private Button loadButton;
-
- private Button saveButton;
-
- private Button closeButton;
-
- private Key key;
-
- private boolean autoCloseOnStop = false;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- super.setContentView(R.layout.secure_note);
- this.noteText = (EditText)super.findViewById(R.id.note_text);
- this.loadButton = (Button)super.findViewById(R.id.load_button);
- this.saveButton = (Button)super.findViewById(R.id.save_button);
- this.closeButton = (Button)super.findViewById(R.id.close_button);
- this.loadButton.setOnClickListener(this);
- this.saveButton.setOnClickListener(this);
- this.closeButton.setOnClickListener(this);
- this.noteText.addTextChangedListener(this);
- if (savedInstanceState != null) {
- this.key = (Key)savedInstanceState.getSerializable(PASSWORD_KEY); // ???
- }
- }
-
- protected void onPostCreate(Bundle savedInstanceState) {
- super.onPostCreate(savedInstanceState);
- if (this.noteText.length() == 0) {
- if (this.isSecureNoteFilePresent()) {
- this.saveButton.setVisibility(View.GONE);
- this.noteText.setEnabled(false);
- this.getPassword(GET_PASSWORD_FOR_LOAD, false);
- } else {
- this.loadButton.setVisibility(View.GONE);
- this.saveButton.setEnabled(false);
- }
- } else {
- this.loadButton.setVisibility(View.GONE);
- }
- }
-
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putSerializable(PASSWORD_KEY, this.key); // ???
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- super.getMenuInflater().inflate(R.menu.secure_note, menu);
- return true;
- }
-
- @Override
- public boolean onPrepareOptionsMenu(Menu menu) {
- menu.findItem(R.id.change_password_button).setEnabled(this.key != null);
- menu.findItem(R.id.delete_button).setEnabled(this.isSecureNoteFilePresent());
- return true;
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.change_password_button:
- this.getPassword(CHANGE_PASSWORD, true);
- return true;
- 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) {
- SecureNoteActivity.this.deleteSecureNote();
- }
- }).setNegativeButton(android.R.string.no, null).show();
- return true;
- default:
- return super.onOptionsItemSelected(item);
- }
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- if (this.autoCloseOnStop && !this.saveButton.isEnabled()) {
- Log.d(TAG, "Auto closing");
- SecureNoteActivity.this.finish();
- }
- }
-
- 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);
- super.startActivityForResult(intent, requestCode);
- }
-
- private boolean isSecureNoteFilePresent() {
- return super.getFileStreamPath(FILENAME).exists();
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- switch (resultCode) {
- case RESULT_OK:
- this.key = (Key)data
- .getSerializableExtra(GetPasswordActivity.PASSWORD_RESPONSE_PARAM);
- switch (requestCode) {
- case GET_PASSWORD_FOR_LOAD:
- this.loadSecureNote();
- break;
- case GET_PASSWORD_FOR_SAVE:
- this.saveSecureNote();
- break;
- case CHANGE_PASSWORD:
- Log.d(TAG, "Changed password (in memory)");
- this.toast(R.string.changed_password);
- this.saveButton.setEnabled(true);
- break;
- }
- break;
- case RESULT_CANCELED:
- Log.d(TAG, "Canceled result. Ignoring.");
- break;
- default:
- Log.w(TAG, "Unexpected result: " + resultCode);
- }
- }
-
- public void onClick(View v) {
- if (v == this.loadButton) {
- if (this.key == null) {
- this.getPassword(GET_PASSWORD_FOR_LOAD, false);
- } else {
- this.loadSecureNote();
- }
- } else if (v == this.saveButton) {
- if (this.key == null) {
- this.getPassword(GET_PASSWORD_FOR_SAVE, true);
- } else {
- this.saveSecureNote();
- }
- } else if (v == this.closeButton) {
- Log.d(TAG, "Closing...");
- if (this.saveButton.isEnabled()) {
- new AlertDialog.Builder(this)
- .setMessage(R.string.close_alert)
- .setCancelable(false)
- .setPositiveButton(android.R.string.yes,
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int id) {
- SecureNoteActivity.this.finish();
- }
- }).setNegativeButton(android.R.string.no, null).show();
- } else {
- super.finish();
- }
- }
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- this.key = null;
- this.noteText.getText().clear();
- this.noteText = null;
- System.exit(0);
- }
-
- private void deleteSecureNote() {
- Log.d(TAG, "Deleteting note");
- if (super.getFileStreamPath(FILENAME).delete()) {
- this.key = null;
- this.noteText.getText().clear();
- this.loadButton.setVisibility(View.GONE);
- this.saveButton.setVisibility(View.VISIBLE);
- this.saveButton.setEnabled(false);
- this.noteText.setEnabled(true);
- this.toast(R.string.deleted_note);
- Log.d(TAG, "Deleted note");
- } else {
- this.toast(R.string.failed_to_delete);
- Log.e(TAG, "Failed to delete note");
- }
- }
-
- private void saveSecureNote() {
- Log.d(TAG, "Saving note");
- new AsyncTask() {
-
- @Override
- protected Boolean doInBackground(String... strings) {
- try {
- OutputStream out = SecureNoteActivity.super.openFileOutput(FILENAME,
- MODE_PRIVATE);
- try {
- Cipher cipher = CryptUtil.getEncryptCipher(SecureNoteActivity.this.key);
- byte[] iv = CryptUtil.getIv(cipher);
- out.write(iv);
- out = new CipherOutputStream(out, cipher);
- Writer writer = new OutputStreamWriter(out, CHARSET);
- for (String string : strings) {
- writer.write(string);
- }
- writer.flush();
- } finally {
- out.close();
- }
- Log.d(TAG, "Saved note to " + FILENAME);
- return true;
- } catch (Exception e) {
- Log.e(TAG, "Failed to save note to " + FILENAME, e);
- SecureNoteActivity.super.getFileStreamPath(FILENAME).delete();
- return false;
- }
- }
-
- @Override
- protected void onPostExecute(Boolean result) {
- if (result) {
- SecureNoteActivity.this.toast(R.string.saved_note);
- SecureNoteActivity.this.saveButton.setEnabled(false);
- } else {
- SecureNoteActivity.this.toast(R.string.failed_to_save);
- }
- }
-
- }.execute(this.noteText.getText().toString());
- }
-
- private void loadSecureNote() {
- Log.d(TAG, "Loading note...");
- new AsyncTask() {
- @Override
- protected String doInBackground(Void... params) {
- try {
- InputStream in = SecureNoteActivity.super.openFileInput(FILENAME);
- try {
- byte[] iv = CryptUtil.getIv(in);
- Cipher cipher = CryptUtil.getDecryptCipher(SecureNoteActivity.this.key, iv);
- in = new CipherInputStream(in, cipher);
- Reader reader = new InputStreamReader(in, CHARSET);
- StringBuilder out = new StringBuilder(1024);
- CharBuffer buffer = CharBuffer.allocate(64);
- for (int n; (n = reader.read(buffer)) != -1;) {
- buffer.flip();
- out.append(buffer, 0, n);
- }
- Log.d(TAG, "Loaded note from " + FILENAME);
- return out.toString();
- } finally {
- in.close();
- }
- } catch (Exception e) {
- Log.e(TAG, "Failed to load note from " + FILENAME, e);
- return null;
- }
- }
-
- @Override
- protected void onPostExecute(String result) {
- if (result == null) {
- SecureNoteActivity.this.toast(R.string.failed_to_load);
- } else {
- SecureNoteActivity.this.noteText.setText(result);
- SecureNoteActivity.this.toast(R.string.loaded_note);
- SecureNoteActivity.this.loadButton.setVisibility(View.GONE);
- SecureNoteActivity.this.saveButton.setVisibility(View.VISIBLE);
- SecureNoteActivity.this.saveButton.setEnabled(false);
- SecureNoteActivity.this.noteText.setEnabled(true);
- }
- }
- }.execute();
- }
-
- private void toast(int resId) {
- Toast.makeText(this, resId, Toast.LENGTH_LONG).show();
- }
-
- public void afterTextChanged(Editable e) {
- this.saveButton.setEnabled(true);
- }
-
- public void beforeTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
- // ignored
- }
-
- public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
- // ignored
-
- }
-}