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 @@ + + + + + + + + + +