diff --git a/README.md b/README.md index 9b98d1ce..39138f47 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ So make sure you have a **current backup** before switching! * **Requesting thumbnails**: If you are missing a thumbnail you can request it by editing the [wiki](https://github.com/andOTP/andOTP/wiki/Thumbnails#thumbnail-requests) * **Discussion and support**: - [XDA thread](https://forum.xda-developers.com/android/apps-games/app-andotp-android-otp-authenticator-t3636993) (please keep off-topic to a minimum) - - Telegram channel [@andOTP](https://t.me/andOTP) + - Telegram group [@andOTP](https://t.me/andOTP) (also check out the read-only announcement channel for important updates: [@andOTP_Broadcast](https://t.me/andOTP_Broadcast)) #### Contributors: diff --git a/app/src/androidTest/java/org/shadowice/flocke/andotp/ApplicationTest.java b/app/src/androidTest/java/org/shadowice/flocke/andotp/ApplicationTest.java index a63777ab..3c2577b0 100644 --- a/app/src/androidTest/java/org/shadowice/flocke/andotp/ApplicationTest.java +++ b/app/src/androidTest/java/org/shadowice/flocke/andotp/ApplicationTest.java @@ -32,8 +32,10 @@ import org.apache.commons.codec.binary.Hex; import org.json.JSONException; import org.json.JSONObject; import org.shadowice.flocke.andotp.Database.Entry; +import org.shadowice.flocke.andotp.Utilities.Constants; import org.shadowice.flocke.andotp.Utilities.DatabaseHelper; import org.shadowice.flocke.andotp.Utilities.EncryptionHelper; +import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper; import org.shadowice.flocke.andotp.Utilities.TokenCalculator; import java.io.File; @@ -52,11 +54,10 @@ import java.util.Arrays; import javax.crypto.BadPaddingException; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; -import static org.shadowice.flocke.andotp.Utilities.TokenCalculator.TOTP_DEFAULT_PERIOD; - public class ApplicationTest extends ApplicationTestCase { public ApplicationTest() { @@ -69,29 +70,29 @@ public class ApplicationTest extends ApplicationTestCase { byte[] keySHA256 = "12345678901234567890123456789012".getBytes(StandardCharsets.US_ASCII); byte[] keySHA512 = "1234567890123456789012345678901234567890123456789012345678901234".getBytes(StandardCharsets.US_ASCII); - assertEquals(94287082, TokenCalculator.TOTP_RFC6238(keySHA1, TOTP_DEFAULT_PERIOD, 59L, 8, TokenCalculator.HashAlgorithm.SHA1)); - assertEquals(46119246, TokenCalculator.TOTP_RFC6238(keySHA256, TOTP_DEFAULT_PERIOD, 59L, 8, TokenCalculator.HashAlgorithm.SHA256)); - assertEquals(90693936, TokenCalculator.TOTP_RFC6238(keySHA512, TOTP_DEFAULT_PERIOD, 59L, 8, TokenCalculator.HashAlgorithm.SHA512)); + assertEquals(94287082, TokenCalculator.TOTP_RFC6238(keySHA1, TokenCalculator.TOTP_DEFAULT_PERIOD, 59L, 8, TokenCalculator.HashAlgorithm.SHA1)); + assertEquals(46119246, TokenCalculator.TOTP_RFC6238(keySHA256, TokenCalculator.TOTP_DEFAULT_PERIOD, 59L, 8, TokenCalculator.HashAlgorithm.SHA256)); + assertEquals(90693936, TokenCalculator.TOTP_RFC6238(keySHA512, TokenCalculator.TOTP_DEFAULT_PERIOD, 59L, 8, TokenCalculator.HashAlgorithm.SHA512)); - assertEquals(7081804, TokenCalculator.TOTP_RFC6238(keySHA1, TOTP_DEFAULT_PERIOD, 1111111109L, 8, TokenCalculator.HashAlgorithm.SHA1)); - assertEquals(68084774, TokenCalculator.TOTP_RFC6238(keySHA256, TOTP_DEFAULT_PERIOD, 1111111109L, 8, TokenCalculator.HashAlgorithm.SHA256)); - assertEquals(25091201, TokenCalculator.TOTP_RFC6238(keySHA512, TOTP_DEFAULT_PERIOD, 1111111109L, 8, TokenCalculator.HashAlgorithm.SHA512)); + assertEquals(7081804, TokenCalculator.TOTP_RFC6238(keySHA1, TokenCalculator.TOTP_DEFAULT_PERIOD, 1111111109L, 8, TokenCalculator.HashAlgorithm.SHA1)); + assertEquals(68084774, TokenCalculator.TOTP_RFC6238(keySHA256, TokenCalculator.TOTP_DEFAULT_PERIOD, 1111111109L, 8, TokenCalculator.HashAlgorithm.SHA256)); + assertEquals(25091201, TokenCalculator.TOTP_RFC6238(keySHA512, TokenCalculator.TOTP_DEFAULT_PERIOD, 1111111109L, 8, TokenCalculator.HashAlgorithm.SHA512)); - assertEquals(14050471, TokenCalculator.TOTP_RFC6238(keySHA1, TOTP_DEFAULT_PERIOD, 1111111111L, 8, TokenCalculator.HashAlgorithm.SHA1)); - assertEquals(67062674, TokenCalculator.TOTP_RFC6238(keySHA256, TOTP_DEFAULT_PERIOD, 1111111111L, 8, TokenCalculator.HashAlgorithm.SHA256)); - assertEquals(99943326, TokenCalculator.TOTP_RFC6238(keySHA512, TOTP_DEFAULT_PERIOD, 1111111111L, 8, TokenCalculator.HashAlgorithm.SHA512)); + assertEquals(14050471, TokenCalculator.TOTP_RFC6238(keySHA1, TokenCalculator.TOTP_DEFAULT_PERIOD, 1111111111L, 8, TokenCalculator.HashAlgorithm.SHA1)); + assertEquals(67062674, TokenCalculator.TOTP_RFC6238(keySHA256, TokenCalculator.TOTP_DEFAULT_PERIOD, 1111111111L, 8, TokenCalculator.HashAlgorithm.SHA256)); + assertEquals(99943326, TokenCalculator.TOTP_RFC6238(keySHA512, TokenCalculator.TOTP_DEFAULT_PERIOD, 1111111111L, 8, TokenCalculator.HashAlgorithm.SHA512)); - assertEquals(89005924, TokenCalculator.TOTP_RFC6238(keySHA1, TOTP_DEFAULT_PERIOD, 1234567890L, 8, TokenCalculator.HashAlgorithm.SHA1)); - assertEquals(91819424, TokenCalculator.TOTP_RFC6238(keySHA256, TOTP_DEFAULT_PERIOD, 1234567890L, 8, TokenCalculator.HashAlgorithm.SHA256)); - assertEquals(93441116, TokenCalculator.TOTP_RFC6238(keySHA512, TOTP_DEFAULT_PERIOD, 1234567890L, 8, TokenCalculator.HashAlgorithm.SHA512)); + assertEquals(89005924, TokenCalculator.TOTP_RFC6238(keySHA1, TokenCalculator.TOTP_DEFAULT_PERIOD, 1234567890L, 8, TokenCalculator.HashAlgorithm.SHA1)); + assertEquals(91819424, TokenCalculator.TOTP_RFC6238(keySHA256, TokenCalculator.TOTP_DEFAULT_PERIOD, 1234567890L, 8, TokenCalculator.HashAlgorithm.SHA256)); + assertEquals(93441116, TokenCalculator.TOTP_RFC6238(keySHA512, TokenCalculator.TOTP_DEFAULT_PERIOD, 1234567890L, 8, TokenCalculator.HashAlgorithm.SHA512)); - assertEquals(69279037, TokenCalculator.TOTP_RFC6238(keySHA1, TOTP_DEFAULT_PERIOD, 2000000000L, 8, TokenCalculator.HashAlgorithm.SHA1)); - assertEquals(90698825, TokenCalculator.TOTP_RFC6238(keySHA256, TOTP_DEFAULT_PERIOD, 2000000000L, 8, TokenCalculator.HashAlgorithm.SHA256)); - assertEquals(38618901, TokenCalculator.TOTP_RFC6238(keySHA512, TOTP_DEFAULT_PERIOD, 2000000000L, 8, TokenCalculator.HashAlgorithm.SHA512)); + assertEquals(69279037, TokenCalculator.TOTP_RFC6238(keySHA1, TokenCalculator.TOTP_DEFAULT_PERIOD, 2000000000L, 8, TokenCalculator.HashAlgorithm.SHA1)); + assertEquals(90698825, TokenCalculator.TOTP_RFC6238(keySHA256, TokenCalculator.TOTP_DEFAULT_PERIOD, 2000000000L, 8, TokenCalculator.HashAlgorithm.SHA256)); + assertEquals(38618901, TokenCalculator.TOTP_RFC6238(keySHA512, TokenCalculator.TOTP_DEFAULT_PERIOD, 2000000000L, 8, TokenCalculator.HashAlgorithm.SHA512)); - assertEquals(65353130, TokenCalculator.TOTP_RFC6238(keySHA1, TOTP_DEFAULT_PERIOD, 20000000000L, 8, TokenCalculator.HashAlgorithm.SHA1)); - assertEquals(77737706, TokenCalculator.TOTP_RFC6238(keySHA256, TOTP_DEFAULT_PERIOD, 20000000000L, 8, TokenCalculator.HashAlgorithm.SHA256)); - assertEquals(47863826, TokenCalculator.TOTP_RFC6238(keySHA512, TOTP_DEFAULT_PERIOD, 20000000000L, 8, TokenCalculator.HashAlgorithm.SHA512)); + assertEquals(65353130, TokenCalculator.TOTP_RFC6238(keySHA1, TokenCalculator.TOTP_DEFAULT_PERIOD, 20000000000L, 8, TokenCalculator.HashAlgorithm.SHA1)); + assertEquals(77737706, TokenCalculator.TOTP_RFC6238(keySHA256, TokenCalculator.TOTP_DEFAULT_PERIOD, 20000000000L, 8, TokenCalculator.HashAlgorithm.SHA256)); + assertEquals(47863826, TokenCalculator.TOTP_RFC6238(keySHA512, TokenCalculator.TOTP_DEFAULT_PERIOD, 20000000000L, 8, TokenCalculator.HashAlgorithm.SHA512)); } @@ -106,6 +107,8 @@ public class ApplicationTest extends ApplicationTestCase { "\"digits\":6," + "\"type\":\"TOTP\"," + "\"algorithm\":\"SHA1\"," + + "\"thumbnail\":\"Default\"," + + "\"last_used\":0," + "\"tags\":[\"test1\",\"test2\"]}"; Entry e = new Entry(new JSONObject(s)); @@ -170,10 +173,11 @@ public class ApplicationTest extends ApplicationTestCase { keyStore.load(null); keyStore.deleteEntry("settings"); - new File(context.getFilesDir() + "/" + DatabaseHelper.SETTINGS_FILE).delete(); - new File(context.getFilesDir() + "/" + DatabaseHelper.KEY_FILE).delete(); + new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE).delete(); + new File(context.getFilesDir() + "/" + Constants.FILENAME_ENCRYPTED_KEY).delete(); - ArrayList b = DatabaseHelper.loadDatabase(context); + SecretKey encryptionKey = KeyStoreHelper.loadEncryptionKeyFromKeyStore(context); + ArrayList b = DatabaseHelper.loadDatabase(context, encryptionKey); assertEquals(0, b.size()); ArrayList a = new ArrayList<>(); @@ -187,13 +191,13 @@ public class ApplicationTest extends ApplicationTestCase { e.setSecret("secret2".getBytes()); a.add(e); - DatabaseHelper.saveDatabase(context, a); - b = DatabaseHelper.loadDatabase(context); + DatabaseHelper.saveDatabase(context, a, encryptionKey); + b = DatabaseHelper.loadDatabase(context, encryptionKey); assertEquals(a, b); - new File(context.getFilesDir() + "/" + DatabaseHelper.SETTINGS_FILE).delete(); - new File(context.getFilesDir() + "/" + DatabaseHelper.KEY_FILE).delete(); + new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE).delete(); + new File(context.getFilesDir() + "/" + Constants.FILENAME_ENCRYPTED_KEY).delete(); } public void testEncryptionHelper() throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, UnsupportedEncodingException, InvalidAlgorithmParameterException, DecoderException { diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/AuthenticateActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/AuthenticateActivity.java index 23b44379..9343b7cc 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/AuthenticateActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/AuthenticateActivity.java @@ -29,11 +29,13 @@ import android.support.design.widget.TextInputLayout; import android.support.v7.widget.Toolbar; import android.text.InputType; import android.text.method.PasswordTransformationMethod; +import android.util.Base64; import android.view.KeyEvent; import android.view.View; import android.view.ViewStub; import android.view.WindowManager; import android.view.inputmethod.EditorInfo; +import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; @@ -41,13 +43,25 @@ import android.widget.Toast; import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.digest.DigestUtils; import org.shadowice.flocke.andotp.R; +import org.shadowice.flocke.andotp.Utilities.Constants; +import org.shadowice.flocke.andotp.Utilities.EncryptionHelper; -import static org.shadowice.flocke.andotp.Utilities.Settings.AuthMethod; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; + +import static org.shadowice.flocke.andotp.Utilities.Constants.AuthMethod; public class AuthenticateActivity extends ThemedActivity - implements EditText.OnEditorActionListener { + implements EditText.OnEditorActionListener, View.OnClickListener { private String password; + AuthMethod authMethod; + String newEncryption = ""; + boolean oldPassword = false; + + TextInputEditText passwordInput; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -63,65 +77,114 @@ public class AuthenticateActivity extends ThemedActivity stub.setLayoutResource(R.layout.content_authenticate); View v = stub.inflate(); + Intent callingIntent = getIntent(); + int labelMsg = callingIntent.getIntExtra(Constants.EXTRA_AUTH_MESSAGE, R.string.auth_msg_authenticate); + newEncryption = callingIntent.getStringExtra(Constants.EXTRA_AUTH_NEW_ENCRYPTION); + TextView passwordLabel = v.findViewById(R.id.passwordLabel); TextInputLayout passwordLayout = v.findViewById(R.id.passwordLayout); - TextInputEditText passwordInput = v.findViewById(R.id.passwordEdit); + passwordInput = v.findViewById(R.id.passwordEdit); - AuthMethod authMethod = settings.getAuthMethod(); + passwordLabel.setText(labelMsg); + + authMethod = settings.getAuthMethod(); + password = settings.getAuthCredentials(); + + if (password.isEmpty()) { + password = settings.getOldCredentials(authMethod); + oldPassword = true; + } if (authMethod == AuthMethod.PASSWORD) { - password = settings.getAuthPasswordHash(); - if (password.isEmpty()) { Toast.makeText(this, R.string.auth_toast_password_missing, Toast.LENGTH_LONG).show(); - finishWithResult(true); + finishWithResult(true, null); } else { - passwordLabel.setText(R.string.auth_msg_password); passwordLayout.setHint(getString(R.string.auth_hint_password)); passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); } } else if (authMethod == AuthMethod.PIN) { - password = settings.getAuthPINHash(); - if (password.isEmpty()) { Toast.makeText(this, R.string.auth_toast_pin_missing, Toast.LENGTH_LONG).show(); - finishWithResult(true); + finishWithResult(true, null); } else { - passwordLabel.setText(R.string.auth_msg_pin); passwordLayout.setHint(getString(R.string.auth_hint_pin)); passwordInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); } } else { - finishWithResult(true); + finishWithResult(true, null); } passwordInput.setTransformationMethod(new PasswordTransformationMethod()); passwordInput.setOnEditorActionListener(this); + Button unlockButton = v.findViewById(R.id.buttonUnlock); + unlockButton.setOnClickListener(this); + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); } + @Override + public void onClick(View view) { + checkPassword(passwordInput.getText().toString()); + } + @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (actionId == EditorInfo.IME_ACTION_DONE) { - String hashedPassword = new String(Hex.encodeHex(DigestUtils.sha256(v.getText().toString()))); - - if (hashedPassword.equals(password)) { - finishWithResult(true); - } else { - finishWithResult(false); - } - + checkPassword(v.getText().toString()); return true; } return false; } + public void checkPassword(String plainPassword) { + if (! oldPassword) { + try { + EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(plainPassword, settings.getSalt(), settings.getIterations()); + byte[] passwordArray = Base64.decode(password, Base64.URL_SAFE); + + if (Arrays.equals(passwordArray, credentials.password)) { + finishWithResult(true, credentials.key); + } else { + finishWithResult(false, null); + } + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + e.printStackTrace(); + finishWithResult(false, null); + } + } else { + String hashedPassword = new String(Hex.encodeHex(DigestUtils.sha256(plainPassword))); + + if (hashedPassword.equals(password)) { + byte[] key = settings.setAuthCredentials(password); + + if (key == null) + Toast.makeText(this, R.string.settings_toast_auth_upgrade_failed, Toast.LENGTH_LONG).show(); + + if (authMethod == AuthMethod.PASSWORD) + settings.removeAuthPasswordHash(); + else if (authMethod == AuthMethod.PIN) + settings.removeAuthPINHash(); + + finishWithResult(true, key); + } else { + finishWithResult(false, null); + } + } + } + // End with a result - public void finishWithResult(boolean success) { + public void finishWithResult(boolean success, byte[] key) { Intent data = new Intent(); + if (newEncryption != null && ! newEncryption.isEmpty()) + data.putExtra(Constants.EXTRA_AUTH_NEW_ENCRYPTION, newEncryption); + + if (key != null) + data.putExtra(Constants.EXTRA_AUTH_PASSWORD_KEY, key); + if (success) setResult(RESULT_OK, data); @@ -131,7 +194,7 @@ public class AuthenticateActivity extends ThemedActivity // Go back to the main activity @Override public void onBackPressed() { - finishWithResult(false); + finishWithResult(false, null); super.onBackPressed(); } } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java index 41414e69..ee9f04a6 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/BackupActivity.java @@ -50,6 +50,7 @@ import org.openintents.openpgp.util.OpenPgpApi; import org.openintents.openpgp.util.OpenPgpServiceConnection; import org.shadowice.flocke.andotp.Database.Entry; import org.shadowice.flocke.andotp.R; +import org.shadowice.flocke.andotp.Utilities.Constants; import org.shadowice.flocke.andotp.Utilities.DatabaseHelper; import org.shadowice.flocke.andotp.Utilities.EncryptionHelper; import org.shadowice.flocke.andotp.Utilities.FileHelper; @@ -64,28 +65,7 @@ import java.util.ArrayList; import javax.crypto.SecretKey; public class BackupActivity extends BaseActivity { - private final static int INTENT_OPEN_DOCUMENT_PLAIN = 200; - private final static int INTENT_SAVE_DOCUMENT_PLAIN = 201; - private final static int INTENT_OPEN_DOCUMENT_CRYPT = 202; - private final static int INTENT_SAVE_DOCUMENT_CRYPT = 203; - private final static int INTENT_OPEN_DOCUMENT_PGP = 204; - private final static int INTENT_SAVE_DOCUMENT_PGP = 205; - private final static int INTENT_ENCRYPT_PGP = 206; - private final static int INTENT_DECRYPT_PGP = 207; - - private final static int PERMISSIONS_REQUEST_READ_IMPORT_PLAIN = 210; - private final static int PERMISSIONS_REQUEST_WRITE_EXPORT_PLAIN = 211; - private final static int PERMISSIONS_REQUEST_READ_IMPORT_CRYPT = 212; - private final static int PERMISSIONS_REQUEST_WRITE_EXPORT_CRYPT = 213; - private final static int PERMISSIONS_REQUEST_READ_IMPORT_PGP = 214; - private final static int PERMISSIONS_REQUEST_WRITE_EXPORT_PGP = 215; - - private static final String DEFAULT_BACKUP_FILENAME_PLAIN = "otp_accounts.json"; - private static final String DEFAULT_BACKUP_FILENAME_CRYPT = "otp_accounts.json.aes"; - private static final String DEFAULT_BACKUP_FILENAME_PGP = "otp_accounts.json.gpg"; - private static final String DEFAULT_BACKUP_MIMETYPE_PLAIN = "application/json"; - private static final String DEFAULT_BACKUP_MIMETYPE_CRYPT = "binary/aes"; - private static final String DEFAULT_BACKUP_MIMETYPE_PGP = "application/pgp-encrypted"; + private SecretKey encryptionKey = null; private OpenPgpServiceConnection pgpServiceConnection; private long pgpKeyId; @@ -111,6 +91,10 @@ public class BackupActivity extends BaseActivity { stub.setLayoutResource(R.layout.content_backup); View v = stub.inflate(); + Intent callingIntent = getIntent(); + byte[] keyMaterial = callingIntent.getByteArrayExtra(Constants.EXTRA_BACKUP_ENCRYPTION_KEY); + encryptionKey = EncryptionHelper.generateSymmetricKey(keyMaterial); + // Plain-text LinearLayout backupPlain = v.findViewById(R.id.button_backup_plain); @@ -126,7 +110,7 @@ public class BackupActivity extends BaseActivity { restorePlain.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - openFileWithPermissions(INTENT_OPEN_DOCUMENT_PLAIN, PERMISSIONS_REQUEST_READ_IMPORT_PLAIN); + openFileWithPermissions(Constants.INTENT_BACKUP_OPEN_DOCUMENT_PLAIN, Constants.PERMISSIONS_BACKUP_READ_IMPORT_PLAIN); } }); @@ -149,14 +133,14 @@ public class BackupActivity extends BaseActivity { backupCrypt.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - saveFileWithPermissions(DEFAULT_BACKUP_MIMETYPE_CRYPT, DEFAULT_BACKUP_FILENAME_CRYPT, INTENT_SAVE_DOCUMENT_CRYPT, PERMISSIONS_REQUEST_WRITE_EXPORT_CRYPT); + saveFileWithPermissions(Constants.BACKUP_MIMETYPE_CRYPT, Constants.BACKUP_FILENAME_CRYPT, Constants.INTENT_BACKUP_SAVE_DOCUMENT_CRYPT, Constants.PERMISSIONS_BACKUP_WRITE_EXPORT_CRYPT); } }); restoreCrypt.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - openFileWithPermissions(INTENT_OPEN_DOCUMENT_CRYPT, PERMISSIONS_REQUEST_READ_IMPORT_CRYPT); + openFileWithPermissions(Constants.INTENT_BACKUP_OPEN_DOCUMENT_CRYPT, Constants.PERMISSIONS_BACKUP_READ_IMPORT_CRYPT); } }); @@ -184,14 +168,14 @@ public class BackupActivity extends BaseActivity { backupPGP.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - saveFileWithPermissions(DEFAULT_BACKUP_MIMETYPE_PGP, DEFAULT_BACKUP_FILENAME_PGP, INTENT_SAVE_DOCUMENT_PGP, PERMISSIONS_REQUEST_WRITE_EXPORT_PGP); + saveFileWithPermissions(Constants.BACKUP_MIMETYPE_PGP, Constants.BACKUP_FILENAME_PGP, Constants.INTENT_BACKUP_SAVE_DOCUMENT_PGP, Constants.PERMISSIONS_BACKUP_WRITE_EXPORT_PGP); } }); restorePGP.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - openFileWithPermissions(INTENT_OPEN_DOCUMENT_PGP, PERMISSIONS_REQUEST_READ_IMPORT_PGP); + openFileWithPermissions(Constants.INTENT_BACKUP_OPEN_DOCUMENT_PGP, Constants.PERMISSIONS_BACKUP_READ_IMPORT_PGP); } }); } @@ -232,39 +216,39 @@ public class BackupActivity extends BaseActivity { // Get the result from permission requests @Override public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { - if (requestCode == PERMISSIONS_REQUEST_READ_IMPORT_PLAIN) { + if (requestCode == Constants.PERMISSIONS_BACKUP_READ_IMPORT_PLAIN) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - showOpenFileSelector(INTENT_OPEN_DOCUMENT_PLAIN); + showOpenFileSelector(Constants.INTENT_BACKUP_OPEN_DOCUMENT_PLAIN); } else { Toast.makeText(this, R.string.backup_toast_storage_permissions, Toast.LENGTH_LONG).show(); } - } else if (requestCode == PERMISSIONS_REQUEST_WRITE_EXPORT_PLAIN) { + } else if (requestCode == Constants.PERMISSIONS_BACKUP_WRITE_EXPORT_PLAIN) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - showSaveFileSelector(DEFAULT_BACKUP_MIMETYPE_PLAIN, DEFAULT_BACKUP_FILENAME_PLAIN, INTENT_SAVE_DOCUMENT_PLAIN); + showSaveFileSelector(Constants.BACKUP_MIMETYPE_PLAIN, Constants.BACKUP_FILENAME_PLAIN, Constants.INTENT_BACKUP_SAVE_DOCUMENT_PLAIN); } else { Toast.makeText(this, R.string.backup_toast_storage_permissions, Toast.LENGTH_LONG).show(); } - } else if (requestCode == PERMISSIONS_REQUEST_READ_IMPORT_CRYPT) { + } else if (requestCode == Constants.PERMISSIONS_BACKUP_READ_IMPORT_CRYPT) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - showOpenFileSelector(INTENT_OPEN_DOCUMENT_CRYPT); + showOpenFileSelector(Constants.INTENT_BACKUP_OPEN_DOCUMENT_CRYPT); } else { Toast.makeText(this, R.string.backup_toast_storage_permissions, Toast.LENGTH_LONG).show(); } - } else if (requestCode == PERMISSIONS_REQUEST_WRITE_EXPORT_CRYPT) { + } else if (requestCode == Constants.PERMISSIONS_BACKUP_WRITE_EXPORT_CRYPT) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - showSaveFileSelector(DEFAULT_BACKUP_MIMETYPE_CRYPT, DEFAULT_BACKUP_FILENAME_CRYPT, INTENT_SAVE_DOCUMENT_CRYPT); + showSaveFileSelector(Constants.BACKUP_MIMETYPE_CRYPT, Constants.BACKUP_FILENAME_CRYPT, Constants.INTENT_BACKUP_SAVE_DOCUMENT_CRYPT); } else { Toast.makeText(this, R.string.backup_toast_storage_permissions, Toast.LENGTH_LONG).show(); } - } else if (requestCode == PERMISSIONS_REQUEST_READ_IMPORT_PGP) { + } else if (requestCode == Constants.PERMISSIONS_BACKUP_READ_IMPORT_PGP) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - showOpenFileSelector(INTENT_OPEN_DOCUMENT_PGP); + showOpenFileSelector(Constants.INTENT_BACKUP_OPEN_DOCUMENT_PGP); } else { Toast.makeText(this, R.string.backup_toast_storage_permissions, Toast.LENGTH_LONG).show(); } - } else if (requestCode == PERMISSIONS_REQUEST_WRITE_EXPORT_PGP) { + } else if (requestCode == Constants.PERMISSIONS_BACKUP_WRITE_EXPORT_PGP) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - showSaveFileSelector(DEFAULT_BACKUP_MIMETYPE_PGP, DEFAULT_BACKUP_FILENAME_PGP, INTENT_SAVE_DOCUMENT_PGP); + showSaveFileSelector(Constants.BACKUP_MIMETYPE_PGP, Constants.BACKUP_FILENAME_PGP, Constants.INTENT_BACKUP_SAVE_DOCUMENT_PGP); } else { Toast.makeText(this, R.string.backup_toast_storage_permissions, Toast.LENGTH_LONG).show(); } @@ -278,31 +262,31 @@ public class BackupActivity extends BaseActivity { protected void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); - if (requestCode == INTENT_OPEN_DOCUMENT_PLAIN && resultCode == RESULT_OK) { + if (requestCode == Constants.INTENT_BACKUP_OPEN_DOCUMENT_PLAIN && resultCode == RESULT_OK) { if (intent != null) { doRestorePlain(intent.getData()); } - } else if (requestCode == INTENT_SAVE_DOCUMENT_PLAIN && resultCode == RESULT_OK) { + } else if (requestCode == Constants.INTENT_BACKUP_SAVE_DOCUMENT_PLAIN && resultCode == RESULT_OK) { if (intent != null) { doBackupPlain(intent.getData()); } - } else if (requestCode == INTENT_OPEN_DOCUMENT_CRYPT && resultCode == RESULT_OK) { + } else if (requestCode == Constants.INTENT_BACKUP_OPEN_DOCUMENT_CRYPT && resultCode == RESULT_OK) { if (intent != null) { doRestoreCrypt(intent.getData()); } - } else if (requestCode == INTENT_SAVE_DOCUMENT_CRYPT && resultCode == RESULT_OK) { + } else if (requestCode == Constants.INTENT_BACKUP_SAVE_DOCUMENT_CRYPT && resultCode == RESULT_OK) { if (intent != null) { doBackupCrypt(intent.getData()); } - } else if (requestCode == INTENT_OPEN_DOCUMENT_PGP && resultCode == RESULT_OK) { + } else if (requestCode == Constants.INTENT_BACKUP_OPEN_DOCUMENT_PGP && resultCode == RESULT_OK) { if (intent != null) restoreEncryptedWithPGP(intent.getData(), null); - } else if (requestCode == INTENT_SAVE_DOCUMENT_PGP && resultCode == RESULT_OK) { + } else if (requestCode == Constants.INTENT_BACKUP_SAVE_DOCUMENT_PGP && resultCode == RESULT_OK) { if (intent != null) backupEncryptedWithPGP(intent.getData(), null); - } else if (requestCode == INTENT_ENCRYPT_PGP && resultCode == RESULT_OK) { + } else if (requestCode == Constants.INTENT_BACKUP_ENCRYPT_PGP && resultCode == RESULT_OK) { backupEncryptedWithPGP(encryptTargetFile, intent); - } else if (requestCode == INTENT_DECRYPT_PGP && resultCode == RESULT_OK) { + } else if (requestCode == Constants.INTENT_BACKUP_DECRYPT_PGP && resultCode == RESULT_OK) { restoreEncryptedWithPGP(decryptSourceFile, intent); } } @@ -316,12 +300,12 @@ public class BackupActivity extends BaseActivity { intent.setType("*/*"); startActivityForResult(intent, intentId); } else { - if (intentId == INTENT_OPEN_DOCUMENT_PLAIN) - doRestorePlain(Tools.buildUri(settings.getBackupDir(), DEFAULT_BACKUP_FILENAME_PLAIN)); - else if (intentId == INTENT_OPEN_DOCUMENT_CRYPT) - doRestoreCrypt(Tools.buildUri(settings.getBackupDir(), DEFAULT_BACKUP_FILENAME_CRYPT)); - else if (intentId == INTENT_OPEN_DOCUMENT_PGP) - restoreEncryptedWithPGP(Tools.buildUri(settings.getBackupDir(), DEFAULT_BACKUP_FILENAME_PGP), null); + if (intentId == Constants.INTENT_BACKUP_OPEN_DOCUMENT_PLAIN) + doRestorePlain(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_PLAIN)); + else if (intentId == Constants.INTENT_BACKUP_OPEN_DOCUMENT_CRYPT) + doRestoreCrypt(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_CRYPT)); + else if (intentId == Constants.INTENT_BACKUP_OPEN_DOCUMENT_PGP) + restoreEncryptedWithPGP(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_PGP), null); } } @@ -334,12 +318,12 @@ public class BackupActivity extends BaseActivity { startActivityForResult(intent, intentId); } else { if (Tools.mkdir(settings.getBackupDir())) { - if (intentId == INTENT_SAVE_DOCUMENT_PLAIN) - doBackupPlain(Tools.buildUri(settings.getBackupDir(), DEFAULT_BACKUP_FILENAME_PLAIN)); - else if (intentId == INTENT_SAVE_DOCUMENT_CRYPT) - doBackupCrypt(Tools.buildUri(settings.getBackupDir(), DEFAULT_BACKUP_FILENAME_CRYPT)); - else if (intentId == INTENT_SAVE_DOCUMENT_PGP) - backupEncryptedWithPGP(Tools.buildUri(settings.getBackupDir(), DEFAULT_BACKUP_FILENAME_PGP), null); + if (intentId == Constants.INTENT_BACKUP_SAVE_DOCUMENT_PLAIN) + doBackupPlain(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_PLAIN)); + else if (intentId == Constants.INTENT_BACKUP_SAVE_DOCUMENT_CRYPT) + doBackupCrypt(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_CRYPT)); + else if (intentId == Constants.INTENT_BACKUP_SAVE_DOCUMENT_PGP) + backupEncryptedWithPGP(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_PGP), null); } else { Toast.makeText(this, R.string.backup_toast_mkdir_failed, Toast.LENGTH_LONG).show(); } @@ -367,13 +351,13 @@ public class BackupActivity extends BaseActivity { if (entries.size() > 0) { if (! replace.isChecked()) { - ArrayList currentEntries = DatabaseHelper.loadDatabase(this); + ArrayList currentEntries = DatabaseHelper.loadDatabase(this, encryptionKey); entries.removeAll(currentEntries); entries.addAll(currentEntries); } - if (DatabaseHelper.saveDatabase(this, entries)) { + if (DatabaseHelper.saveDatabase(this, entries, encryptionKey)) { reload = true; Toast.makeText(this, R.string.backup_toast_import_success, Toast.LENGTH_LONG).show(); finishWithResult(); @@ -399,9 +383,9 @@ public class BackupActivity extends BaseActivity { private void doBackupPlain(Uri uri) { if (Tools.isExternalStorageWritable()) { - boolean success = DatabaseHelper.exportAsJSON(this, uri); + ArrayList entries = DatabaseHelper.loadDatabase(this, encryptionKey); - if (success) + if (FileHelper.writeStringToFile(this, uri, DatabaseHelper.entriesToString(entries))) Toast.makeText(this, R.string.backup_toast_export_success, Toast.LENGTH_LONG).show(); else Toast.makeText(this, R.string.backup_toast_export_failed, Toast.LENGTH_LONG).show(); @@ -420,7 +404,7 @@ public class BackupActivity extends BaseActivity { .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { - saveFileWithPermissions(DEFAULT_BACKUP_MIMETYPE_PLAIN, DEFAULT_BACKUP_FILENAME_PLAIN, INTENT_SAVE_DOCUMENT_PLAIN, PERMISSIONS_REQUEST_WRITE_EXPORT_PLAIN); + saveFileWithPermissions(Constants.BACKUP_MIMETYPE_PLAIN, Constants.BACKUP_FILENAME_PLAIN, Constants.INTENT_BACKUP_SAVE_DOCUMENT_PLAIN, Constants.PERMISSIONS_BACKUP_WRITE_EXPORT_PLAIN); } }) .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { @@ -472,7 +456,7 @@ public class BackupActivity extends BaseActivity { if (! password.isEmpty()) { if (Tools.isExternalStorageWritable()) { - ArrayList entries = DatabaseHelper.loadDatabase(this); + ArrayList entries = DatabaseHelper.loadDatabase(this, encryptionKey); String plain = DatabaseHelper.entriesToString(entries); boolean success = true; @@ -515,7 +499,7 @@ public class BackupActivity extends BaseActivity { ByteArrayOutputStream os = new ByteArrayOutputStream(); OpenPgpApi api = new OpenPgpApi(this, pgpServiceConnection.getService()); Intent result = api.executeApi(decryptIntent, is, os); - handleOpenPGPResult(result, os, uri, INTENT_DECRYPT_PGP); + handleOpenPGPResult(result, os, uri, Constants.INTENT_BACKUP_DECRYPT_PGP); } private void doBackupEncrypted(Uri uri, String data) { @@ -534,7 +518,7 @@ public class BackupActivity extends BaseActivity { } private void backupEncryptedWithPGP(Uri uri, Intent encryptIntent) { - ArrayList entries = DatabaseHelper.loadDatabase(this); + ArrayList entries = DatabaseHelper.loadDatabase(this, encryptionKey); String plainJSON = DatabaseHelper.entriesToString(entries); if (encryptIntent == null) { @@ -555,7 +539,7 @@ public class BackupActivity extends BaseActivity { ByteArrayOutputStream os = new ByteArrayOutputStream(); OpenPgpApi api = new OpenPgpApi(this, pgpServiceConnection.getService()); Intent result = api.executeApi(encryptIntent, is, os); - handleOpenPGPResult(result, os, uri, INTENT_ENCRYPT_PGP); + handleOpenPGPResult(result, os, uri, Constants.INTENT_BACKUP_ENCRYPT_PGP); } public String outputStreamToString(ByteArrayOutputStream os) { @@ -564,10 +548,10 @@ public class BackupActivity extends BaseActivity { public void handleOpenPGPResult(Intent result, ByteArrayOutputStream os, Uri file, int requestCode) { if (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR) == OpenPgpApi.RESULT_CODE_SUCCESS) { - if (requestCode == INTENT_ENCRYPT_PGP) { + if (requestCode == Constants.INTENT_BACKUP_ENCRYPT_PGP) { if (os != null) doBackupEncrypted(file, outputStreamToString(os)); - } else if (requestCode == INTENT_DECRYPT_PGP) { + } else if (requestCode == Constants.INTENT_BACKUP_DECRYPT_PGP) { if (os != null) { if (settings.getOpenPGPVerify()) { OpenPgpSignatureResult sigResult = result.getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE); @@ -586,9 +570,9 @@ public class BackupActivity extends BaseActivity { PendingIntent pi = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT); // Small hack to keep the target file even after user interaction - if (requestCode == INTENT_ENCRYPT_PGP) { + if (requestCode == Constants.INTENT_BACKUP_ENCRYPT_PGP) { encryptTargetFile = file; - } else if (requestCode == INTENT_DECRYPT_PGP) { + } else if (requestCode == Constants.INTENT_BACKUP_DECRYPT_PGP) { decryptSourceFile = file; } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java index 6cffff8a..353b8693 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java @@ -58,8 +58,9 @@ import com.google.zxing.integration.android.IntentResult; import org.shadowice.flocke.andotp.Database.Entry; import org.shadowice.flocke.andotp.R; -import org.shadowice.flocke.andotp.Utilities.DatabaseHelper; -import org.shadowice.flocke.andotp.Utilities.Settings; +import org.shadowice.flocke.andotp.Utilities.Constants; +import org.shadowice.flocke.andotp.Utilities.EncryptionHelper; +import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper; import org.shadowice.flocke.andotp.Utilities.TokenCalculator; import org.shadowice.flocke.andotp.View.EntriesCardAdapter; import org.shadowice.flocke.andotp.View.FloatingActionMenu; @@ -70,13 +71,14 @@ import org.shadowice.flocke.andotp.View.TagsAdapter; import java.util.ArrayList; import java.util.HashMap; -import static org.shadowice.flocke.andotp.Utilities.Settings.SortMode; +import javax.crypto.SecretKey; + +import static org.shadowice.flocke.andotp.Utilities.Constants.AuthMethod; +import static org.shadowice.flocke.andotp.Utilities.Constants.EncryptionType; +import static org.shadowice.flocke.andotp.Utilities.Constants.SortMode; public class MainActivity extends BaseActivity implements SharedPreferences.OnSharedPreferenceChangeListener { - private static final int INTENT_INTERNAL_AUTHENTICATE = 100; - private static final int INTENT_INTERNAL_SETTINGS = 101; - private static final int INTENT_INTERNAL_BACKUP = 102; private EntriesCardAdapter adapter; private FloatingActionMenu floatingActionMenu; @@ -84,6 +86,7 @@ public class MainActivity extends BaseActivity private MenuItem sortMenu; private SimpleItemTouchHelperCallback touchHelperCallback; + private EncryptionType encryptionType = EncryptionType.KEYSTORE; private boolean requireAuthentication = false; private Handler handler; @@ -103,33 +106,51 @@ public class MainActivity extends BaseActivity private void showFirstTimeWarning() { ViewGroup container = findViewById(R.id.main_content); - View msgView = getLayoutInflater().inflate(R.layout.dialog_security_backup, container, false); + View msgView = getLayoutInflater().inflate(R.layout.dialog_database_encryption, container, false); AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.dialog_title_security_backup) + builder.setTitle(R.string.dialog_title_encryption) .setView(msgView) - .setPositiveButton(R.string.button_warned, new DialogInterface.OnClickListener() { + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { settings.setFirstTimeWarningShown(true); + updateEncryption(null); + } + }) + .setNegativeButton(R.string.button_settings, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + settings.setFirstTimeWarningShown(true); + + Intent settingsIntent = new Intent(getBaseContext(), SettingsActivity.class); + startActivityForResult(settingsIntent, Constants.INTENT_MAIN_SETTINGS); + } + }) + .setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialogInterface) { + settings.setFirstTimeWarningShown(true); + updateEncryption(null); } }) .create() .show(); } - public void authenticate() { - Settings.AuthMethod authMethod = settings.getAuthMethod(); + public void authenticate(int messageId) { + AuthMethod authMethod = settings.getAuthMethod(); - if (authMethod == Settings.AuthMethod.DEVICE) { + if (authMethod == AuthMethod.DEVICE) { KeyguardManager km = (KeyguardManager) getSystemService(KEYGUARD_SERVICE); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP && km.isKeyguardSecure()) { Intent authIntent = km.createConfirmDeviceCredentialIntent(getString(R.string.dialog_title_auth), getString(R.string.dialog_msg_auth)); - startActivityForResult(authIntent, INTENT_INTERNAL_AUTHENTICATE); + startActivityForResult(authIntent, Constants.INTENT_MAIN_AUTHENTICATE); } - } else if (authMethod == Settings.AuthMethod.PASSWORD || authMethod == Settings.AuthMethod.PIN) { + } else if (authMethod == AuthMethod.PASSWORD || authMethod == AuthMethod.PIN) { Intent authIntent = new Intent(this, AuthenticateActivity.class); - startActivityForResult(authIntent, INTENT_INTERNAL_AUTHENTICATE); + authIntent.putExtra(Constants.EXTRA_AUTH_MESSAGE, messageId); + startActivityForResult(authIntent, Constants.INTENT_MAIN_AUTHENTICATE); } } @@ -150,6 +171,23 @@ public class MainActivity extends BaseActivity settings.setSortMode(mode); } + private HashMap createTagsMap(ArrayList entries) { + HashMap tagsHashMap = new HashMap<>(); + + for(Entry entry : entries) { + for(String tag : entry.getTags()) + tagsHashMap.put(tag, settings.getTagToggle(tag)); + } + + return tagsHashMap; + } + + private void populateAdapter() { + adapter.loadEntries(); + tagsDrawerAdapter.setTags(createTagsMap(adapter.getEntries())); + adapter.filterByTags(tagsDrawerAdapter.getActiveTags()); + } + // Initialize the main application @Override protected void onCreate(Bundle savedInstanceState) { @@ -168,13 +206,16 @@ public class MainActivity extends BaseActivity PreferenceManager.setDefaultValues(this, R.xml.preferences, false); settings.registerPreferenceChangeListener(this); - if (savedInstanceState == null) + encryptionType = settings.getEncryption(); + + if (settings.getAuthMethod() != AuthMethod.NONE && savedInstanceState == null) requireAuthentication = true; setBroadcastCallback(new BroadcastReceivedCallback() { @Override public void onReceivedScreenOff() { - requireAuthentication = true; + if (settings.getAuthMethod() != AuthMethod.NONE) + requireAuthentication = true; } }); @@ -203,16 +244,10 @@ public class MainActivity extends BaseActivity llm.setOrientation(LinearLayoutManager.VERTICAL); recList.setLayoutManager(llm); - HashMap tagsHashMap = new HashMap<>(); - for(Entry entry : DatabaseHelper.loadDatabase(this)) { - for(String tag : entry.getTags()) - tagsHashMap.put(tag, settings.getTagToggle(tag)); - } - tagsDrawerAdapter = new TagsAdapter(this, tagsHashMap); - + tagsDrawerAdapter = new TagsAdapter(this, new HashMap()); adapter = new EntriesCardAdapter(this, tagsDrawerAdapter); - recList.setAdapter(adapter); + recList.setAdapter(adapter); recList.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { @@ -293,8 +328,18 @@ public class MainActivity extends BaseActivity super.onResume(); if (requireAuthentication) { - requireAuthentication = false; - authenticate(); + if (settings.getAuthMethod() != AuthMethod.NONE) { + requireAuthentication = false; + authenticate(R.string.auth_msg_authenticate); + } + } else { + if (settings.getFirstTimeWarningShown()) { + if (adapter.getEncryptionKey() == null) { + updateEncryption(null); + } else { + populateAdapter(); + } + } } startUpdater(); @@ -344,14 +389,20 @@ public class MainActivity extends BaseActivity Toast.makeText(this, R.string.toast_invalid_qr_code, Toast.LENGTH_LONG).show(); } } - } else if (requestCode == INTENT_INTERNAL_BACKUP && resultCode == RESULT_OK) { + } else if (requestCode == Constants.INTENT_MAIN_BACKUP && resultCode == RESULT_OK) { if (intent.getBooleanExtra("reload", false)) { adapter.loadEntries(); refreshTags(); } - } else if (requestCode == INTENT_INTERNAL_AUTHENTICATE) { + } else if (requestCode == Constants.INTENT_MAIN_SETTINGS && resultCode == RESULT_OK) { + boolean encryptionChanged = intent.getBooleanExtra(Constants.EXTRA_SETTINGS_ENCRYPTION_CHANGED, false); + byte[] newKey = intent.getByteArrayExtra(Constants.EXTRA_SETTINGS_ENCRYPTION_KEY); + + if (encryptionChanged) + updateEncryption(newKey); + } else if (requestCode == Constants.INTENT_MAIN_AUTHENTICATE) { if (resultCode != RESULT_OK) { - Toast.makeText(getBaseContext(), R.string.toast_auth_failed, Toast.LENGTH_LONG).show(); + Toast.makeText(getBaseContext(), R.string.toast_auth_failed_fatal, Toast.LENGTH_LONG).show(); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { finishAndRemoveTask(); @@ -360,10 +411,34 @@ public class MainActivity extends BaseActivity } } else { requireAuthentication = false; + + byte[] authKey = intent.getByteArrayExtra(Constants.EXTRA_AUTH_PASSWORD_KEY); + updateEncryption(authKey); } } } + private void updateEncryption(byte[] newKey) { + SecretKey encryptionKey = null; + + encryptionType = settings.getEncryption(); + + if (encryptionType == EncryptionType.KEYSTORE) { + encryptionKey = KeyStoreHelper.loadEncryptionKeyFromKeyStore(this, false); + } else if (encryptionType == EncryptionType.PASSWORD) { + if (newKey != null && newKey.length > 0) { + encryptionKey = EncryptionHelper.generateSymmetricKey(newKey); + } else { + authenticate(R.string.auth_msg_confirm_encryption); + } + } + + if (encryptionKey != null) + adapter.setEncryptionKey(encryptionKey); + + populateAdapter(); + } + // Options menu @Override public boolean onCreateOptionsMenu(Menu menu) { @@ -438,10 +513,13 @@ public class MainActivity extends BaseActivity if (id == R.id.action_backup) { Intent backupIntent = new Intent(this, BackupActivity.class); - startActivityForResult(backupIntent, INTENT_INTERNAL_BACKUP); + backupIntent.putExtra(Constants.EXTRA_BACKUP_ENCRYPTION_KEY, adapter.getEncryptionKey().getEncoded()); + startActivityForResult(backupIntent, Constants.INTENT_MAIN_BACKUP); } else if (id == R.id.action_settings) { Intent settingsIntent = new Intent(this, SettingsActivity.class); - startActivityForResult(settingsIntent, INTENT_INTERNAL_SETTINGS); + if (adapter.getEncryptionKey() != null) + settingsIntent.putExtra(Constants.EXTRA_SETTINGS_ENCRYPTION_KEY, adapter.getEncryptionKey().getEncoded()); + startActivityForResult(settingsIntent, Constants.INTENT_MAIN_SETTINGS); } else if (id == R.id.action_about){ Intent aboutIntent = new Intent(this, AboutActivity.class); startActivity(aboutIntent); diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/PanicResponderActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/PanicResponderActivity.java index c927e17e..9b999c11 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/PanicResponderActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/PanicResponderActivity.java @@ -27,11 +27,10 @@ import android.content.Intent; import android.os.Build; import android.os.Bundle; -import org.shadowice.flocke.andotp.Database.Entry; import org.shadowice.flocke.andotp.Utilities.DatabaseHelper; +import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper; import org.shadowice.flocke.andotp.Utilities.Settings; -import java.util.ArrayList; import java.util.Set; public class PanicResponderActivity extends Activity { @@ -47,8 +46,10 @@ public class PanicResponderActivity extends Activity { Set response = settings.getPanicResponse(); - if (response.contains("accounts")) - DatabaseHelper.saveDatabase(this, new ArrayList()); + if (response.contains("accounts")) { + DatabaseHelper.wipeDatabase(this); + KeyStoreHelper.wipeKeys(this); + } if (response.contains("settings")) settings.clear(true); diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/SettingsActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/SettingsActivity.java index a1e1e547..ebc75912 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/SettingsActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/SettingsActivity.java @@ -25,6 +25,8 @@ package org.shadowice.flocke.andotp.Activities; import android.app.KeyguardManager; import android.app.backup.BackupManager; import android.app.backup.RestoreObserver; +import android.app.AlertDialog; +import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; @@ -40,13 +42,30 @@ import android.widget.Toast; import org.openintents.openpgp.util.OpenPgpAppPreference; import org.openintents.openpgp.util.OpenPgpKeyPreference; -import org.shadowice.flocke.andotp.Preferences.PasswordHashPreference; +import org.shadowice.flocke.andotp.Database.Entry; +import org.shadowice.flocke.andotp.Preferences.CredentialsPreference; import org.shadowice.flocke.andotp.R; +import org.shadowice.flocke.andotp.Utilities.Constants; +import org.shadowice.flocke.andotp.Utilities.DatabaseHelper; +import org.shadowice.flocke.andotp.Utilities.EncryptionHelper; +import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper; +import org.shadowice.flocke.andotp.Utilities.Settings; +import org.shadowice.flocke.andotp.Utilities.UIHelper; + +import java.util.ArrayList; + +import javax.crypto.SecretKey; + +import static org.shadowice.flocke.andotp.Utilities.Constants.AuthMethod; +import static org.shadowice.flocke.andotp.Utilities.Constants.EncryptionType; public class SettingsActivity extends BaseActivity implements SharedPreferences.OnSharedPreferenceChangeListener{ SettingsFragment fragment; + SecretKey encryptionKey = null; + boolean encryptionChanged = false; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -60,6 +79,11 @@ public class SettingsActivity extends BaseActivity ViewStub stub = findViewById(R.id.container_stub); stub.inflate(); + Intent callingIntent = getIntent(); + byte[] keyMaterial = callingIntent.getByteArrayExtra(Constants.EXTRA_SETTINGS_ENCRYPTION_KEY); + if (keyMaterial != null && keyMaterial.length > 0) + encryptionKey = EncryptionHelper.generateSymmetricKey(keyMaterial); + fragment = new SettingsFragment(); getFragmentManager().beginTransaction() @@ -71,7 +95,13 @@ public class SettingsActivity extends BaseActivity } public void finishWithResult() { - setResult(RESULT_OK); + Intent data = new Intent(); + + data.putExtra(Constants.EXTRA_SETTINGS_ENCRYPTION_CHANGED, encryptionChanged); + if (encryptionKey != null) + data.putExtra(Constants.EXTRA_SETTINGS_ENCRYPTION_KEY, encryptionKey.getEncoded()); + + setResult(RESULT_OK, data); finish(); } @@ -98,92 +128,146 @@ public class SettingsActivity extends BaseActivity } } + private void generateNewEncryptionKey() { + if (settings.getEncryption() == EncryptionType.KEYSTORE) { + encryptionKey = KeyStoreHelper.loadEncryptionKeyFromKeyStore(this, false); + encryptionChanged = true; + } + } + + private void tryEncryptionChangeWithAuth(EncryptionType newEnc) { + Intent authIntent = new Intent(this, AuthenticateActivity.class); + authIntent.putExtra(Constants.EXTRA_AUTH_NEW_ENCRYPTION, newEnc.name()); + authIntent.putExtra(Constants.EXTRA_AUTH_MESSAGE, R.string.auth_msg_confirm_encryption); + startActivityForResult(authIntent, Constants.INTENT_SETTINGS_AUTHENTICATE); + } + + private boolean tryEncryptionChange(EncryptionType newEnc, byte[] newKey) { + Toast upgrading = Toast.makeText(this, R.string.settings_toast_encryption_changing, Toast.LENGTH_LONG); + upgrading.show(); + + if (DatabaseHelper.backupDatabase(this)) { + ArrayList entries; + + if (encryptionKey != null) + entries = DatabaseHelper.loadDatabase(this, encryptionKey); + else + entries = new ArrayList<>(); + + SecretKey newEncryptionKey; + + if (newEnc == EncryptionType.KEYSTORE) { + newEncryptionKey = KeyStoreHelper.loadEncryptionKeyFromKeyStore(this, true); + } else if (newKey != null && newKey.length > 0) { + newEncryptionKey = EncryptionHelper.generateSymmetricKey(newKey); + } else { + upgrading.cancel(); + DatabaseHelper.restoreDatabaseBackup(this); + return false; + } + + if (DatabaseHelper.saveDatabase(this, entries, newEncryptionKey)) { + encryptionKey = newEncryptionKey; + encryptionChanged = true; + + fragment.encryption.setValue(newEnc.name().toLowerCase()); + + upgrading.cancel(); + Toast.makeText(this, R.string.settings_toast_encryption_change_success, Toast.LENGTH_LONG).show(); + + return true; + } + + DatabaseHelper.restoreDatabaseBackup(this); + + upgrading.cancel(); + Toast.makeText(this, R.string.settings_toast_encryption_change_failed, Toast.LENGTH_LONG).show(); + } else { + upgrading.cancel(); + Toast.makeText(this, R.string.settings_toast_encryption_backup_failed, Toast.LENGTH_LONG).show(); + } + + return false; + } + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - if (fragment.pgpKey.handleOnActivityResult(requestCode, resultCode, data)) { + + if (requestCode == Constants.INTENT_SETTINGS_AUTHENTICATE) { + if (resultCode == RESULT_OK) { + byte[] authKey = data.getByteArrayExtra(Constants.EXTRA_AUTH_PASSWORD_KEY); + String newEnc = data.getStringExtra(Constants.EXTRA_AUTH_NEW_ENCRYPTION); + + if (authKey != null && authKey.length > 0 && newEnc != null && !newEnc.isEmpty()) { + EncryptionType newEncType = EncryptionType.valueOf(newEnc); + tryEncryptionChange(newEncType, authKey); + } else { + Toast.makeText(this, R.string.settings_toast_encryption_no_key, Toast.LENGTH_LONG).show(); + } + } else { + Toast.makeText(this, R.string.settings_toast_encryption_auth_failed, Toast.LENGTH_LONG).show(); + } + } else if (fragment.pgpKey.handleOnActivityResult(requestCode, resultCode, data)) { // handled by OpenPgpKeyPreference return; + } } - } public static class SettingsFragment extends PreferenceFragment { PreferenceCategory catSecurity; + Settings settings; + ListPreference encryption; + OpenPgpAppPreference pgpProvider; OpenPgpKeyPreference pgpKey; - public void updateAuthPassword(String newAuth) { - PasswordHashPreference pwPref = (PasswordHashPreference) catSecurity.findPreference(getString(R.string.settings_key_auth_password_hash)); - PasswordHashPreference pinPref = (PasswordHashPreference) catSecurity.findPreference(getString(R.string.settings_key_auth_pin_hash)); - - if (pwPref != null) - catSecurity.removePreference(pwPref); - if (pinPref != null) - catSecurity.removePreference(pinPref); - - switch (newAuth) { - case "password": - PasswordHashPreference authPassword = new PasswordHashPreference(getActivity(), null); - authPassword.setTitle(R.string.settings_title_auth_password); - authPassword.setOrder(4); - authPassword.setKey(getString(R.string.settings_key_auth_password_hash)); - authPassword.setMode(PasswordHashPreference.Mode.PASSWORD); - - catSecurity.addPreference(authPassword); - - break; - - case "pin": - PasswordHashPreference authPIN = new PasswordHashPreference(getActivity(), null); - authPIN.setTitle(R.string.settings_title_auth_pin); - authPIN.setOrder(4); - authPIN.setKey(getString(R.string.settings_key_auth_pin_hash)); - authPIN.setMode(PasswordHashPreference.Mode.PIN); - - catSecurity.addPreference(authPIN); - - break; - - default: - break; - } - } - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getActivity().getBaseContext()); + settings = new Settings(getActivity()); + final SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getActivity().getBaseContext()); addPreferencesFromResource(R.xml.preferences); + CredentialsPreference credentialsPreference = (CredentialsPreference) findPreference(getString(R.string.settings_key_auth)); + credentialsPreference.setEncryptionChangeCallback(new CredentialsPreference.EncryptionChangeCallback() { + @Override + public boolean testEncryptionChange(byte[] newKey) { + return ((SettingsActivity) getActivity()).tryEncryptionChange(settings.getEncryption(), newKey); + } + }); + // Authentication catSecurity = (PreferenceCategory) findPreference(getString(R.string.settings_key_cat_security)); - ListPreference authPref = (ListPreference) findPreference(getString(R.string.settings_key_auth)); + encryption = (ListPreference) findPreference(getString(R.string.settings_key_encryption)); - updateAuthPassword(authPref.getValue()); - - authPref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + encryption.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override - public boolean onPreferenceChange(Preference preference, Object o) { - String newAuth = (String) o; + public boolean onPreferenceChange(final Preference preference, Object o) { + String newEncryption = (String) o; + EncryptionType encryptionType = EncryptionType.valueOf(newEncryption.toUpperCase()); + AuthMethod authMethod = settings.getAuthMethod(); - if (newAuth.equals("device")) { - KeyguardManager km = (KeyguardManager) getActivity().getSystemService(KEYGUARD_SERVICE); - - if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) { - Toast.makeText(getActivity(), R.string.settings_toast_auth_device_pre_lollipop, Toast.LENGTH_LONG).show(); + if (encryptionType == EncryptionType.PASSWORD) { + if (authMethod != AuthMethod.PASSWORD && authMethod != AuthMethod.PIN) { + UIHelper.showGenericDialog(getActivity(), R.string.settings_dialog_title_error, R.string.settings_dialog_msg_encryption_invalid_with_auth); return false; - } else if (! km.isKeyguardSecure()) { - Toast.makeText(getActivity(), R.string.settings_toast_auth_device_not_secure, Toast.LENGTH_LONG).show(); + } else { + if (settings.getAuthCredentials().isEmpty()) { + UIHelper.showGenericDialog(getActivity(), R.string.settings_dialog_title_error, R.string.settings_dialog_msg_encryption_invalid_without_credentials); return false; } } - updateAuthPassword(newAuth); + ((SettingsActivity) getActivity()).tryEncryptionChangeWithAuth(encryptionType); + } else if (encryptionType == EncryptionType.KEYSTORE) { + ((SettingsActivity) getActivity()).tryEncryptionChange(encryptionType, null); + } - return true; + return false; } }); @@ -205,6 +289,40 @@ public class SettingsActivity extends BaseActivity if (sharedPref.contains(getString(R.string.settings_key_special_features)) && sharedPref.getBoolean(getString(R.string.settings_key_special_features), false)) { addPreferencesFromResource(R.xml.preferences_special); + + Preference clearKeyStore = findPreference(getString(R.string.settings_key_clear_keystore)); + clearKeyStore.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + + builder.setTitle(R.string.settings_dialog_title_clear_keystore); + if (settings.getEncryption() == EncryptionType.PASSWORD) + builder.setMessage(R.string.settings_dialog_msg_clear_keystore_password); + else if (settings.getEncryption() == EncryptionType.KEYSTORE) + builder.setMessage(R.string.settings_dialog_msg_clear_keystore_keystore); + + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + KeyStoreHelper.wipeKeys(getActivity()); + if (settings.getEncryption() == EncryptionType.KEYSTORE) { + DatabaseHelper.wipeDatabase(getActivity()); + ((SettingsActivity) getActivity()).generateNewEncryptionKey(); + } + } + }); + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + } + }); + + builder.create().show(); + + return false; + } + }); } } } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Database/Entry.java b/app/src/main/java/org/shadowice/flocke/andotp/Database/Entry.java index 2c1eb332..51fe31e8 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Database/Entry.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Database/Entry.java @@ -41,7 +41,9 @@ import java.util.Objects; import java.util.Set; public class Entry { - public enum OTPType { TOTP, STEAM} + public enum OTPType { + TOTP, STEAM + } public static Set PublicTypes = EnumSet.of(OTPType.TOTP); private static final OTPType DEFAULT_TYPE = OTPType.TOTP; diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Preferences/CredentialsPreference.java b/app/src/main/java/org/shadowice/flocke/andotp/Preferences/CredentialsPreference.java new file mode 100644 index 00000000..51b718a0 --- /dev/null +++ b/app/src/main/java/org/shadowice/flocke/andotp/Preferences/CredentialsPreference.java @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2017 Jakob Nixdorf + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.shadowice.flocke.andotp.Preferences; + +import android.app.AlertDialog; +import android.app.KeyguardManager; +import android.content.Context; +import android.preference.DialogPreference; +import android.support.design.widget.TextInputEditText; +import android.support.design.widget.TextInputLayout; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.text.method.PasswordTransformationMethod; +import android.util.AttributeSet; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import org.shadowice.flocke.andotp.R; +import org.shadowice.flocke.andotp.Utilities.Constants; +import org.shadowice.flocke.andotp.Utilities.Settings; +import org.shadowice.flocke.andotp.Utilities.UIHelper; + +import java.util.Arrays; +import java.util.List; + +import static android.content.Context.KEYGUARD_SERVICE; +import static org.shadowice.flocke.andotp.Utilities.Constants.AuthMethod; +import static org.shadowice.flocke.andotp.Utilities.Constants.EncryptionType; + +public class CredentialsPreference extends DialogPreference + implements AdapterView.OnItemClickListener, View.OnClickListener, TextWatcher { + public static final AuthMethod DEFAULT_VALUE = AuthMethod.NONE; + + public interface EncryptionChangeCallback { + boolean testEncryptionChange(byte[] newKey); + } + + private List entries; + private static final List entryValues = Arrays.asList( + AuthMethod.NONE, + AuthMethod.PASSWORD, + AuthMethod.PIN, + AuthMethod.DEVICE + ); + + private int minLength = 0; + + private Settings settings; + private AuthMethod value = AuthMethod.NONE; + private EncryptionChangeCallback encryptionChangeCallback = null; + + private LinearLayout credentialsLayout; + private TextInputLayout passwordLayout; + private TextInputEditText passwordInput; + private EditText passwordConfirm; + private TextView toShortWarning; + + private Button btnSave; + + public CredentialsPreference(Context context, AttributeSet attrs) { + super(context, attrs); + + settings = new Settings(context); + entries = Arrays.asList(context.getResources().getStringArray(R.array.settings_entries_auth)); + + setDialogLayoutResource(R.layout.component_authentication); + } + + public void setEncryptionChangeCallback(EncryptionChangeCallback cb) { + this.encryptionChangeCallback = cb; + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + super.onPrepareDialogBuilder(builder); + + builder.setPositiveButton(null, null); + builder.setNegativeButton(null, null); + } + + @Override + protected void onBindDialogView(View view) { + value = settings.getAuthMethod(); + + ListView listView = view.findViewById(R.id.credentialSelection); + + ArrayAdapter adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_single_choice, entries); + listView.setAdapter(adapter); + + int index = entryValues.indexOf(value); + listView.setSelection(index); + listView.setItemChecked(index,true); + listView.setOnItemClickListener(this); + + credentialsLayout = view.findViewById(R.id.credentialsLayout); + + passwordLayout = view.findViewById(R.id.passwordLayout); + passwordInput = view.findViewById(R.id.passwordEdit); + passwordConfirm = view.findViewById(R.id.passwordConfirm); + + toShortWarning = view.findViewById(R.id.toShortWarning); + + passwordInput.addTextChangedListener(this); + passwordConfirm.addTextChangedListener(this); + + Button btnCancel = view.findViewById(R.id.btnCancel); + btnSave = view.findViewById(R.id.btnSave); + + btnCancel.setOnClickListener(this); + btnSave.setOnClickListener(this); + + updateLayout(); + + super.onBindDialogView(view); + } + + @Override + protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) { + if (restorePersistedValue) { + String stringValue = getPersistedString(DEFAULT_VALUE.name().toLowerCase()); + value = AuthMethod.valueOf(stringValue.toUpperCase()); + } else { + value = DEFAULT_VALUE; + persistString(value.name().toLowerCase()); + } + + setSummary(entries.get(entryValues.indexOf(value))); + } + + private void saveValues() { + byte[] newKey = null; + + if (settings.getEncryption() == EncryptionType.PASSWORD) { + if (value == AuthMethod.NONE || value == AuthMethod.DEVICE) { + UIHelper.showGenericDialog(getContext(), R.string.settings_dialog_title_error, R.string.settings_dialog_msg_auth_invalid_with_encryption); + return; + } + } + + if (value == AuthMethod.DEVICE) { + KeyguardManager km = (KeyguardManager) getContext().getSystemService(KEYGUARD_SERVICE); + + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) { + Toast.makeText(getContext(), R.string.settings_toast_auth_device_pre_lollipop, Toast.LENGTH_LONG).show(); + return; + } else if (! km.isKeyguardSecure()) { + Toast.makeText(getContext(), R.string.settings_toast_auth_device_not_secure, Toast.LENGTH_LONG).show(); + return; + } + } + + if (value == AuthMethod.PASSWORD || value == AuthMethod.PIN) { + String password = passwordInput.getText().toString(); + if (!password.isEmpty()) { + newKey = settings.setAuthCredentials(password); + } else { + return; + } + } + + if (settings.getEncryption() == EncryptionType.PASSWORD) { + if (newKey == null || encryptionChangeCallback == null) + return; + + if (! encryptionChangeCallback.testEncryptionChange(newKey)) + return; + } + + persistString(value.toString().toLowerCase()); + setSummary(entries.get(entryValues.indexOf(value))); + } + + @Override + public void onClick(View view) { + switch (view.getId()) { + case (R.id.btnCancel): + getDialog().dismiss(); + break; + case (R.id.btnSave): + saveValues(); + getDialog().dismiss(); + break; + default: + break; + } + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + String password = passwordInput.getEditableText().toString(); + + if (password.length() >= minLength) { + toShortWarning.setVisibility(View.GONE); + + String confirm = passwordConfirm.getEditableText().toString(); + + if (!password.isEmpty() && !confirm.isEmpty() && password.equals(confirm)) { + btnSave.setEnabled(true); + } else { + btnSave.setEnabled(false); + } + } else { + toShortWarning.setVisibility(View.VISIBLE); + } + } + + private void updateLayout() { + if (value == AuthMethod.NONE) { + credentialsLayout.setVisibility(View.GONE); + + if (getDialog() != null) + UIHelper.hideKeyboard(getContext(), getDialog().getCurrentFocus()); + + btnSave.setEnabled(true); + } else if (value == AuthMethod.PASSWORD) { + credentialsLayout.setVisibility(View.VISIBLE); + + passwordLayout.setHint(getContext().getString(R.string.settings_hint_password)); + passwordConfirm.setHint(R.string.settings_hint_password_confirm); + + passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + passwordConfirm.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + + passwordInput.setTransformationMethod(new PasswordTransformationMethod()); + passwordConfirm.setTransformationMethod(new PasswordTransformationMethod()); + + minLength = Constants.AUTH_MIN_PASSWORD_LENGTH; + toShortWarning.setText(getContext().getString(R.string.settings_label_short_password, minLength)); + + passwordInput.requestFocus(); + UIHelper.showKeyboard(getContext(), passwordInput); + + btnSave.setEnabled(false); + } else if (value == AuthMethod.PIN) { + credentialsLayout.setVisibility(View.VISIBLE); + + passwordLayout.setHint(getContext().getString(R.string.settings_hint_pin)); + passwordConfirm.setHint(R.string.settings_hint_pin_confirm); + + passwordInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); + passwordConfirm.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); + + passwordInput.setTransformationMethod(new PasswordTransformationMethod()); + passwordConfirm.setTransformationMethod(new PasswordTransformationMethod()); + + minLength = Constants.AUTH_MIN_PIN_LENGTH; + toShortWarning.setText(getContext().getString(R.string.settings_label_short_pin, minLength)); + + passwordInput.requestFocus(); + UIHelper.showKeyboard(getContext(), passwordInput); + + btnSave.setEnabled(false); + } else if (value == AuthMethod.DEVICE) { + credentialsLayout.setVisibility(View.GONE); + + if (getDialog() != null) + UIHelper.hideKeyboard(getContext(), getDialog().getCurrentFocus()); + + btnSave.setEnabled(true); + } + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + value = entryValues.get(position); + updateLayout(); + } + + // Needed stub functions + @Override + public void afterTextChanged(Editable s) {} + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + +} diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PasswordEncryptedPreference.java b/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PasswordEncryptedPreference.java index b03e5fe7..b287e39a 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PasswordEncryptedPreference.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PasswordEncryptedPreference.java @@ -39,6 +39,7 @@ import android.widget.Button; import android.widget.EditText; import org.shadowice.flocke.andotp.R; +import org.shadowice.flocke.andotp.Utilities.Constants; import org.shadowice.flocke.andotp.Utilities.EncryptionHelper; import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper; @@ -52,7 +53,6 @@ public class PasswordEncryptedPreference extends DialogPreference PASSWORD, PIN } - public static final String KEY_ALIAS = "password"; private KeyPair key; private static final String DEFAULT_VALUE = ""; @@ -70,7 +70,7 @@ public class PasswordEncryptedPreference extends DialogPreference super(context, attrs); try { - key = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, KEY_ALIAS); + key = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, Constants.KEYSTORE_ALIAS_PASSWORD); } catch (Exception e) { e.printStackTrace(); } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PasswordHashPreference.java b/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PasswordHashPreference.java deleted file mode 100644 index bfe8e24b..00000000 --- a/app/src/main/java/org/shadowice/flocke/andotp/Preferences/PasswordHashPreference.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright (C) 2017 Jakob Nixdorf - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.shadowice.flocke.andotp.Preferences; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.res.TypedArray; -import android.preference.DialogPreference; -import android.support.design.widget.TextInputEditText; -import android.support.design.widget.TextInputLayout; -import android.text.Editable; -import android.text.InputType; -import android.text.TextWatcher; -import android.text.method.PasswordTransformationMethod; -import android.util.AttributeSet; -import android.view.View; -import android.widget.Button; -import android.widget.EditText; - -import org.apache.commons.codec.binary.Hex; -import org.apache.commons.codec.digest.DigestUtils; -import org.shadowice.flocke.andotp.R; - -public class PasswordHashPreference extends DialogPreference - implements View.OnClickListener, TextWatcher { - - public enum Mode { - PASSWORD, PIN - } - - private static final String DEFAULT_VALUE = ""; - - private Mode mode = Mode.PASSWORD; - - private TextInputEditText passwordInput; - private EditText passwordConfirm; - - private Button btnSave; - - private String value = DEFAULT_VALUE; - - public PasswordHashPreference(Context context, AttributeSet attrs) { - super(context, attrs); - - setDialogLayoutResource(R.layout.component_password); - } - - public void setMode(Mode mode) { - this.mode = mode; - } - - @Override - protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { - super.onPrepareDialogBuilder(builder); - - builder.setPositiveButton(null, null); - builder.setNegativeButton(null, null); - } - - @Override - protected void onBindDialogView(View view) { - TextInputLayout passwordLayout = view.findViewById(R.id.passwordLayout); - passwordInput = view.findViewById(R.id.passwordEdit); - passwordConfirm = view.findViewById(R.id.passwordConfirm); - - Button btnCancel = view.findViewById(R.id.btnCancel); - btnSave = view.findViewById(R.id.btnSave); - btnSave.setEnabled(false); - - btnCancel.setOnClickListener(this); - btnSave.setOnClickListener(this); - - if (! value.isEmpty()) { - passwordInput.setHint(R.string.settings_hint_unchanged); - } - - if (mode == Mode.PASSWORD) { - passwordLayout.setHint(getContext().getString(R.string.settings_hint_password)); - passwordConfirm.setHint(R.string.settings_hint_password_confirm); - - passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - passwordConfirm.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - } else if (mode == Mode.PIN) { - passwordLayout.setHint(getContext().getString(R.string.settings_hint_pin)); - passwordConfirm.setHint(R.string.settings_hint_pin_confirm); - - passwordInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); - passwordConfirm.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); - } - - passwordInput.setTransformationMethod(new PasswordTransformationMethod()); - passwordConfirm.setTransformationMethod(new PasswordTransformationMethod()); - - passwordConfirm.addTextChangedListener(this); - passwordInput.addTextChangedListener(this); - - super.onBindDialogView(view); - } - - @Override - protected Object onGetDefaultValue(TypedArray a, int index) { - return a.getString(index); - } - - @Override - protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) { - if (restorePersistedValue) { - value = getPersistedString(DEFAULT_VALUE); - } else { - value = (String) defaultValue; - persistString(value); - } - } - - @Override - public void onClick(View view) { - switch (view.getId()) { - case (R.id.btnCancel): - getDialog().dismiss(); - break; - case (R.id.btnSave): - value = passwordInput.getText().toString(); - String hashedPassword = new String(Hex.encodeHex(DigestUtils.sha256(value))); - - persistString(hashedPassword); - - getDialog().dismiss(); - break; - default: - break; - } - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - if (passwordConfirm.getEditableText().toString().equals(passwordInput.getEditableText().toString())) { - btnSave.setEnabled(true); - } else { - btnSave.setEnabled(false); - } - } - - @Override - public void afterTextChanged(Editable s) {} - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) {} -} diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Constants.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Constants.java new file mode 100644 index 00000000..d8ac8716 --- /dev/null +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Constants.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2017 Jakob Nixdorf + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.shadowice.flocke.andotp.Utilities; + +import android.os.Environment; + +import java.io.File; + +public class Constants { + // Enums + public enum AuthMethod { + NONE, PASSWORD, PIN, DEVICE + } + + public enum EncryptionType { + KEYSTORE, PASSWORD + } + + public enum SortMode { + UNSORTED, LABEL, LAST_USED + } + + // Intents (Format: A0x with A = parent Activity, x = number of the intent) + public final static int INTENT_MAIN_AUTHENTICATE = 100; + public final static int INTENT_MAIN_SETTINGS = 101; + public final static int INTENT_MAIN_BACKUP = 102; + + public final static int INTENT_BACKUP_OPEN_DOCUMENT_PLAIN = 200; + public final static int INTENT_BACKUP_SAVE_DOCUMENT_PLAIN = 201; + public final static int INTENT_BACKUP_OPEN_DOCUMENT_CRYPT = 202; + public final static int INTENT_BACKUP_SAVE_DOCUMENT_CRYPT = 203; + public final static int INTENT_BACKUP_OPEN_DOCUMENT_PGP = 204; + public final static int INTENT_BACKUP_SAVE_DOCUMENT_PGP = 205; + public final static int INTENT_BACKUP_ENCRYPT_PGP = 206; + public final static int INTENT_BACKUP_DECRYPT_PGP = 207; + + public static final int INTENT_SETTINGS_AUTHENTICATE = 300; + + // Permission requests (Format: A1x with A = parent Activity, x = number of the request) + public final static int PERMISSIONS_BACKUP_READ_IMPORT_PLAIN = 210; + public final static int PERMISSIONS_BACKUP_WRITE_EXPORT_PLAIN = 211; + public final static int PERMISSIONS_BACKUP_READ_IMPORT_CRYPT = 212; + public final static int PERMISSIONS_BACKUP_WRITE_EXPORT_CRYPT = 213; + public final static int PERMISSIONS_BACKUP_READ_IMPORT_PGP = 214; + public final static int PERMISSIONS_BACKUP_WRITE_EXPORT_PGP = 215; + + // Intent extras + public final static String EXTRA_AUTH_PASSWORD_KEY = "password_key"; + public final static String EXTRA_AUTH_NEW_ENCRYPTION = "new_encryption"; + public final static String EXTRA_AUTH_MESSAGE = "message"; + + public final static String EXTRA_BACKUP_ENCRYPTION_KEY = "encryption_key"; + + public final static String EXTRA_SETTINGS_ENCRYPTION_CHANGED = "encryption_changed"; + public final static String EXTRA_SETTINGS_ENCRYPTION_KEY = "encryption_key"; + + // Encryption algorithms and definitions + final static String ALGORITHM_SYMMETRIC = "AES/GCM/NoPadding"; + final static String ALGORITHM_ASYMMETRIC = "RSA/ECB/PKCS1Padding"; + + final static int ENCRYPTION_KEY_LENGTH = 16; + final static int ENCRYPTION_IV_LENGTH = 12; + + final static int PBKDF2_MIN_ITERATIONS = 1000; + final static int PBKDF2_MAX_ITERATIONS = 5000; + final static int PBKDF2_DEFAULT_ITERATIONS = 1000; + final static int PBKDF2_LENGTH = 512; + final static int PBKDF2_SALT_LENGTH = 16; + + // Authentication + public final static int AUTH_MIN_PIN_LENGTH = 4; + public final static int AUTH_MIN_PASSWORD_LENGTH = 6; + + // KeyStore + public final static String KEYSTORE_ALIAS_PASSWORD = "password"; + public final static String KEYSTORE_ALIAS_WRAPPING = "settings"; + + // Database files + public final static String FILENAME_ENCRYPTED_KEY = "otp.key"; + public final static String FILENAME_DATABASE = "secrets.dat"; + public final static String FILENAME_DATABASE_BACKUP = "secrets.dat.bck"; + + // Backup files + public final static String BACKUP_FOLDER = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "andOTP"; + + public final static String BACKUP_FILENAME_PLAIN = "otp_accounts.json"; + public final static String BACKUP_FILENAME_CRYPT = "otp_accounts.json.aes"; + public final static String BACKUP_FILENAME_PGP = "otp_accounts.json.gpg"; + + public final static String BACKUP_MIMETYPE_PLAIN = "application/json"; + public final static String BACKUP_MIMETYPE_CRYPT = "binary/aes"; + public final static String BACKUP_MIMETYPE_PGP = "application/pgp-encrypted"; +} diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java index 6cec3b9f..e09022aa 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java @@ -25,35 +25,90 @@ package org.shadowice.flocke.andotp.Utilities; import android.app.backup.BackupManager; import android.content.Context; -import android.net.Uri; +import android.widget.Toast; import org.json.JSONArray; import org.shadowice.flocke.andotp.Database.Entry; +import org.shadowice.flocke.andotp.R; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import javax.crypto.SecretKey; public class DatabaseHelper { - public static final String KEY_FILE = "otp.key"; - public static final String SETTINGS_FILE = "secrets.dat"; static final Object DatabaseFileLock = new Object(); - /* Database functions */ + public static void wipeDatabase(Context context) { + File db = new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE); + File dbBackup = new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE_BACKUP); + db.delete(); + dbBackup.delete(); + } + + private static void copyFile(File src, File dst) + throws IOException { + try (InputStream in = new FileInputStream(src)) { + try (OutputStream out = new FileOutputStream(dst)) { + byte[] buffer = new byte[1024]; + int len; + while ((len = in.read(buffer)) > 0) { + out.write(buffer, 0, len); + } + } + } + } + + public static boolean backupDatabase(Context context) { + File original = new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE); + File backup = new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE_BACKUP); + + if (original.exists()) { + try { + copyFile(original, backup); + } catch (IOException e) { + return false; + } + } + + return true; + } + + public static boolean restoreDatabaseBackup(Context context) { + File original = new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE); + File backup = new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE_BACKUP); + + if (backup.exists()) { + try { + copyFile(backup, original); + } catch (IOException e) { + return false; + } + } + + return true; + } + + /* Database functions */ + public static boolean saveDatabase(Context context, ArrayList entries, SecretKey encryptionKey) { + if (encryptionKey == null) { + Toast.makeText(context, R.string.toast_encryption_key_empty, Toast.LENGTH_LONG).show(); + return false; + } - public static boolean saveDatabase(Context context, ArrayList entries) { String jsonString = entriesToString(entries); try { synchronized (DatabaseHelper.DatabaseFileLock) { - byte[] data = jsonString.getBytes(); + byte[] data = EncryptionHelper.encrypt(encryptionKey, jsonString.getBytes()); - SecretKey key = KeyStoreHelper.loadOrGenerateWrappedKey(context, new File(context.getFilesDir() + "/" + KEY_FILE)); - data = EncryptionHelper.encrypt(key, data); - - FileHelper.writeBytesToFile(new File(context.getFilesDir() + "/" + SETTINGS_FILE), data); + FileHelper.writeBytesToFile(new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE), data); } } catch (Exception error) { error.printStackTrace(); @@ -66,21 +121,23 @@ public class DatabaseHelper { return true; } - public static ArrayList loadDatabase(Context context){ + public static ArrayList loadDatabase(Context context, SecretKey encryptionKey) { ArrayList entries = new ArrayList<>(); + if (encryptionKey != null) { try { synchronized (DatabaseHelper.DatabaseFileLock) { - byte[] data = FileHelper.readFileToBytes(new File(context.getFilesDir() + "/" + SETTINGS_FILE)); + byte[] data = FileHelper.readFileToBytes(new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE)); + data = EncryptionHelper.decrypt(encryptionKey, data); - SecretKey key = KeyStoreHelper.loadOrGenerateWrappedKey(context, new File(context.getFilesDir() + "/" + KEY_FILE)); - data = EncryptionHelper.decrypt(key, data); - - entries = stringToEntries(new String(data)); + entries = stringToEntries(new String(data)); } } catch (Exception error) { error.printStackTrace(); } + } else { + Toast.makeText(context, R.string.toast_encryption_key_empty, Toast.LENGTH_LONG).show(); + } return entries; } @@ -117,12 +174,4 @@ public class DatabaseHelper { return entries; } - - /* Export functions */ - - public static boolean exportAsJSON(Context context, Uri file) { - ArrayList entries = loadDatabase(context); - - return FileHelper.writeStringToFile(context, file, entriesToString(entries)); - } } \ No newline at end of file diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java index aabff962..fa6628ec 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/EncryptionHelper.java @@ -23,30 +23,71 @@ package org.shadowice.flocke.andotp.Utilities; +import java.io.File; +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; +import java.security.KeyPair; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; import java.util.Arrays; +import java.util.Random; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; public class EncryptionHelper { - private final static String ALGORITHM_SYMMETRIC = "AES/GCM/NoPadding"; - private final static String ALGORITHM_ASYMMETRIC = "RSA/ECB/PKCS1Padding"; + public static class PBKDF2Credentials { + public byte[] password; + public byte[] key; + } - private final static int IV_LENGTH = 12; + public static int generateRandomIterations() { + Random rand = new Random(); + return rand.nextInt((Constants.PBKDF2_MAX_ITERATIONS - Constants.PBKDF2_MIN_ITERATIONS) + 1) + Constants.PBKDF2_MIN_ITERATIONS; + } + + public static byte[] generateRandom(int length) { + final byte[] raw = new byte[length]; + new SecureRandom().nextBytes(raw); + + return raw; + } + + public static PBKDF2Credentials generatePBKDF2Credentials(String password, byte[] salt, int iter) + throws NoSuchAlgorithmException, InvalidKeySpecException { + SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, iter, Constants.PBKDF2_LENGTH); + + byte[] array = secretKeyFactory.generateSecret(keySpec).getEncoded(); + + int halfPoint = array.length / 2; + + PBKDF2Credentials credentials = new PBKDF2Credentials(); + credentials.password = Arrays.copyOfRange(array, halfPoint, array.length); + credentials.key = Arrays.copyOfRange(array, 0, halfPoint); + + return credentials; + } + + public static SecretKey generateSymmetricKey(byte[] data) { + return new SecretKeySpec(data, 0, data.length, "AES"); + } public static SecretKey generateSymmetricKeyFromPassword(String password) throws NoSuchAlgorithmException { @@ -57,7 +98,7 @@ public class EncryptionHelper { public static byte[] encrypt(SecretKey secretKey, IvParameterSpec iv, byte[] plainText) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException { - Cipher cipher = Cipher.getInstance(ALGORITHM_SYMMETRIC); + Cipher cipher = Cipher.getInstance(Constants.ALGORITHM_SYMMETRIC); cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv); return cipher.doFinal(plainText); @@ -65,7 +106,7 @@ public class EncryptionHelper { public static byte[] encrypt(SecretKey secretKey, byte[] plaintext) throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, UnsupportedEncodingException, InvalidAlgorithmParameterException { - final byte[] iv = new byte[IV_LENGTH]; + final byte[] iv = new byte[Constants.ENCRYPTION_IV_LENGTH]; new SecureRandom().nextBytes(iv); byte[] cipherText = encrypt(secretKey, new IvParameterSpec(iv), plaintext); @@ -79,7 +120,7 @@ public class EncryptionHelper { public static byte[] encrypt(PublicKey publicKey, byte[] plaintext) throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, UnsupportedEncodingException, InvalidAlgorithmParameterException { - Cipher cipher = Cipher.getInstance(ALGORITHM_ASYMMETRIC); + Cipher cipher = Cipher.getInstance(Constants.ALGORITHM_ASYMMETRIC); cipher.init(Cipher.ENCRYPT_MODE, publicKey); return cipher.doFinal(plaintext); @@ -87,7 +128,7 @@ public class EncryptionHelper { public static byte[] decrypt(SecretKey secretKey, IvParameterSpec iv, byte[] cipherText) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { - Cipher cipher = Cipher.getInstance(ALGORITHM_SYMMETRIC); + Cipher cipher = Cipher.getInstance(Constants.ALGORITHM_SYMMETRIC); cipher.init(Cipher.DECRYPT_MODE, secretKey, iv); return cipher.doFinal(cipherText); @@ -95,17 +136,44 @@ public class EncryptionHelper { public static byte[] decrypt(SecretKey secretKey, byte[] cipherText) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { - byte[] iv = Arrays.copyOfRange(cipherText, 0, IV_LENGTH); - byte[] encrypted = Arrays.copyOfRange(cipherText, IV_LENGTH, cipherText.length); + byte[] iv = Arrays.copyOfRange(cipherText, 0, Constants.ENCRYPTION_IV_LENGTH); + byte[] encrypted = Arrays.copyOfRange(cipherText, Constants.ENCRYPTION_IV_LENGTH, cipherText.length); return decrypt(secretKey, new IvParameterSpec(iv), encrypted); } public static byte[] decrypt(PrivateKey privateKey, byte[] cipherText) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { - Cipher cipher = Cipher.getInstance(ALGORITHM_ASYMMETRIC); + Cipher cipher = Cipher.getInstance(Constants.ALGORITHM_ASYMMETRIC); cipher.init(Cipher.DECRYPT_MODE, privateKey); return cipher.doFinal(cipherText); } + + /** + * Load our symmetric secret key. + * The symmetric secret key is stored securely on disk by wrapping + * it with a public/private key pair, possibly backed by hardware. + */ + public static SecretKey loadOrGenerateWrappedKey(File keyFile, KeyPair keyPair) + throws GeneralSecurityException, IOException { + final SecretKeyWrapper wrapper = new SecretKeyWrapper(keyPair); + + // Generate secret key if none exists + if (!keyFile.exists()) { + final byte[] raw = EncryptionHelper.generateRandom(Constants.ENCRYPTION_KEY_LENGTH); + + final SecretKey key = new SecretKeySpec(raw, "AES"); + final byte[] wrapped = wrapper.wrap(key); + + + FileHelper.writeBytesToFile(keyFile, wrapped); + } + + // Even if we just generated the key, always read it back to ensure we + // can read it successfully. + final byte[] wrapped = FileHelper.readFileToBytes(keyFile); + + return wrapper.unwrap(wrapped); + } } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java index 6ea4854c..05b275e3 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/KeyStoreHelper.java @@ -28,6 +28,8 @@ import android.security.KeyPairGeneratorSpec; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyProperties; +import org.shadowice.flocke.andotp.R; + import java.io.File; import java.io.IOException; import java.math.BigInteger; @@ -35,17 +37,29 @@ import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; -import java.security.SecureRandom; +import java.security.ProviderException; import java.security.spec.AlgorithmParameterSpec; import java.util.Calendar; import java.util.GregorianCalendar; import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; import javax.security.auth.x500.X500Principal; public class KeyStoreHelper { - private final static int KEY_LENGTH = 16; + + public static void wipeKeys(Context context) { + File keyFile = new File(context.getFilesDir() + "/" + Constants.FILENAME_ENCRYPTED_KEY); + keyFile.delete(); + + try { + final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + if (keyStore.containsAlias(Constants.KEYSTORE_ALIAS_WRAPPING)) + keyStore.deleteEntry(Constants.KEYSTORE_ALIAS_WRAPPING); + } catch (GeneralSecurityException | IOException e) { + e.printStackTrace(); + } + } public static KeyPair loadOrGenerateAsymmetricKeyPair(Context context, String alias) throws GeneralSecurityException, IOException { @@ -83,34 +97,26 @@ public class KeyStoreHelper { } final KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, null); - return new KeyPair(entry.getCertificate().getPublicKey(), entry.getPrivateKey()); + + if (entry != null) + return new KeyPair(entry.getCertificate().getPublicKey(), entry.getPrivateKey()); + else + return null; } - /** - * Load our symmetric secret key. - * The symmetric secret key is stored securely on disk by wrapping - * it with a public/private key pair, possibly backed by hardware. - */ - public static SecretKey loadOrGenerateWrappedKey(Context context, File keyFile) - throws GeneralSecurityException, IOException { - final SecretKeyWrapper wrapper = new SecretKeyWrapper(context, "settings"); + public static SecretKey loadEncryptionKeyFromKeyStore(Context context, boolean failSilent) { + SecretKey encKey = null; - // Generate secret key if none exists - if (!keyFile.exists()) { - final byte[] raw = new byte[KEY_LENGTH]; - new SecureRandom().nextBytes(raw); - - final SecretKey key = new SecretKeySpec(raw, "AES"); - final byte[] wrapped = wrapper.wrap(key); - - - FileHelper.writeBytesToFile(keyFile, wrapped); + try { + KeyPair pair = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, Constants.KEYSTORE_ALIAS_WRAPPING); + if (pair != null) + encKey = EncryptionHelper.loadOrGenerateWrappedKey(new File(context.getFilesDir() + "/" + Constants.FILENAME_ENCRYPTED_KEY), pair); + } catch (GeneralSecurityException | IOException | ProviderException e) { + e.printStackTrace(); + if (! failSilent) + UIHelper.showGenericDialog(context, R.string.dialog_title_keystore_error, R.string.dialog_msg_keystore_error); } - // Even if we just generated the key, always read it back to ensure we - // can read it successfully. - final byte[] wrapped = FileHelper.readFileToBytes(keyFile); - - return wrapper.unwrap(wrapped); + return encKey; } } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/SecretKeyWrapper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/SecretKeyWrapper.java index 0c5ca8f2..bf0b3c4b 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/SecretKeyWrapper.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/SecretKeyWrapper.java @@ -17,7 +17,6 @@ package org.shadowice.flocke.andotp.Utilities; import android.annotation.SuppressLint; -import android.content.Context; import java.io.IOException; import java.security.GeneralSecurityException; @@ -46,10 +45,10 @@ public class SecretKeyWrapper { * If no pair with that alias exists, it will be generated. */ @SuppressLint("GetInstance") - public SecretKeyWrapper(Context context, String alias) + public SecretKeyWrapper(KeyPair keyPair) throws GeneralSecurityException, IOException { - mCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); - mPair = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, alias); + mCipher = Cipher.getInstance(Constants.ALGORITHM_ASYMMETRIC); + mPair = keyPair; } /** diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java index 73ea38a6..dea55a86 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java @@ -24,38 +24,29 @@ package org.shadowice.flocke.andotp.Utilities; import android.content.Context; import android.content.SharedPreferences; -import android.os.Environment; import android.preference.PreferenceManager; import android.util.Base64; -import org.apache.commons.codec.binary.Hex; -import org.apache.commons.codec.digest.DigestUtils; +import org.shadowice.flocke.andotp.Preferences.CredentialsPreference; import org.shadowice.flocke.andotp.R; -import java.io.File; import java.nio.charset.StandardCharsets; import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; import java.util.Collections; import java.util.HashSet; import java.util.Locale; import java.util.Set; -import static org.shadowice.flocke.andotp.Preferences.PasswordEncryptedPreference.KEY_ALIAS; +import static org.shadowice.flocke.andotp.Utilities.Constants.AuthMethod; +import static org.shadowice.flocke.andotp.Utilities.Constants.EncryptionType; +import static org.shadowice.flocke.andotp.Utilities.Constants.SortMode; public class Settings { - private static final String DEFAULT_BACKUP_FOLDER = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "andOTP"; - private Context context; private SharedPreferences settings; - public enum AuthMethod { - NONE, PASSWORD, PIN, DEVICE - } - - public enum SortMode { - UNSORTED, LABEL, LAST_USED - } - public Settings(Context context) { this.context = context; this.settings = PreferenceManager.getDefaultSharedPreferences(context); @@ -67,26 +58,18 @@ public class Settings { private void setupDeviceDependedDefaults() { if (! settings.contains(getResString(R.string.settings_key_backup_directory)) || settings.getString(getResString(R.string.settings_key_backup_directory), "").isEmpty()) { - setString(R.string.settings_key_backup_directory, DEFAULT_BACKUP_FOLDER); + setString(R.string.settings_key_backup_directory, Constants.BACKUP_FOLDER); } } private void migrateDeprecatedSettings() { if (settings.contains(getResString(R.string.settings_key_auth_password))) { - String plainPassword = getAuthPassword(); - String hashedPassword = new String(Hex.encodeHex(DigestUtils.sha256(plainPassword))); - - setString(R.string.settings_key_auth_password_hash, hashedPassword); - + setAuthCredentials(getString(R.string.settings_key_auth_password, "")); remove(R.string.settings_key_auth_password); } if (settings.contains(getResString(R.string.settings_key_auth_pin))) { - String plainPIN = getAuthPIN(); - String hashedPIN = new String(Hex.encodeHex(DigestUtils.sha256(plainPIN))); - - setString(R.string.settings_key_auth_pin_hash, hashedPIN); - + setAuthCredentials(getString(R.string.settings_key_auth_pin, "")); remove(R.string.settings_key_auth_pin); } @@ -94,7 +77,7 @@ public class Settings { String plainPassword = getBackupPassword(); try { - KeyPair key = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, KEY_ALIAS); + KeyPair key = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, Constants.KEYSTORE_ALIAS_PASSWORD); byte[] encPassword = EncryptionHelper.encrypt(key.getPublic(), plainPassword.getBytes(StandardCharsets.UTF_8)); setString(R.string.settings_key_backup_password_enc, Base64.encodeToString(encPassword, Base64.URL_SAFE)); @@ -130,6 +113,10 @@ public class Settings { return settings.getInt(getResString(keyId), getResInt(defaultId)); } + private int getIntValue(int keyId, int defaultValue) { + return settings.getInt(getResString(keyId), defaultValue); + } + private long getLong(int keyId, long defaultValue) { return settings.getLong(getResString(keyId), defaultValue); } @@ -144,6 +131,12 @@ public class Settings { .apply(); } + private void setInt(int keyId, int value) { + settings.edit() + .putInt(getResString(keyId), value) + .apply(); + } + private void setString(int keyId, String value) { settings.edit() .putString(getResString(keyId), value) @@ -163,9 +156,10 @@ public class Settings { } public void clear(boolean keep_auth) { - String authMethod = getAuthMethod().toString().toLowerCase(); - String authPassword = getAuthPasswordHash(); - String authPIN = getAuthPINHash(); + AuthMethod authMethod = getAuthMethod(); + String authCredentials = getAuthCredentials(); + byte[] authSalt = getSalt(); + int authIterations = getIterations(); boolean warningShown = getFirstTimeWarningShown(); @@ -175,13 +169,15 @@ public class Settings { editor.putBoolean(getResString(R.string.settings_key_security_backup_warning), warningShown); if (keep_auth) { - editor.putString(getResString(R.string.settings_key_auth), authMethod); + editor.putString(getResString(R.string.settings_key_auth), authMethod.toString().toLowerCase()); - if (!authPassword.isEmpty()) - editor.putString(getResString(R.string.settings_key_auth_password_hash), authPassword); + if (! authCredentials.isEmpty()) { + editor.putString(getResString(R.string.settings_key_auth_credentials), authCredentials); + editor.putInt(getResString(R.string.settings_key_auth_iterations), authIterations); - if (!authPIN.isEmpty()) - editor.putString(getResString(R.string.settings_key_auth_pin_hash), authPIN); + String encodedSalt = Base64.encodeToString(authSalt, Base64.URL_SAFE); + editor.putString(getResString(R.string.settings_key_auth_salt), encodedSalt); + } } editor.commit(); @@ -206,24 +202,82 @@ public class Settings { } public AuthMethod getAuthMethod() { - String authString = getString(R.string.settings_key_auth, R.string.settings_default_auth); + String authString = getString(R.string.settings_key_auth, CredentialsPreference.DEFAULT_VALUE.name().toLowerCase()); return AuthMethod.valueOf(authString.toUpperCase()); } - public String getAuthPassword() { - return getString(R.string.settings_key_auth_password, ""); + public void removeAuthPasswordHash() { + remove(R.string.settings_key_auth_password_hash); + } + public void removeAuthPINHash() { + remove(R.string.settings_key_auth_pin_hash); } - public String getAuthPasswordHash() { - return getString(R.string.settings_key_auth_password_hash, ""); + public String getOldCredentials(AuthMethod method) { + if (method == AuthMethod.PASSWORD) + return getString(R.string.settings_key_auth_password_hash, ""); + else if (method == AuthMethod.PIN) + return getString(R.string.settings_key_auth_pin_hash, ""); + else + return ""; } - public String getAuthPIN() { - return getString(R.string.settings_key_auth_pin, ""); + public String getAuthCredentials() { + return getString(R.string.settings_key_auth_credentials, ""); } - public String getAuthPINHash() { - return getString(R.string.settings_key_auth_pin_hash, ""); + public byte[] setAuthCredentials(String plainPassword) { + byte[] key = null; + + try { + int iterations = EncryptionHelper.generateRandomIterations(); + EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(plainPassword, getSalt(), iterations); + String password = Base64.encodeToString(credentials.password, Base64.URL_SAFE); + + setIterations(iterations); + setString(R.string.settings_key_auth_credentials, password); + + key = credentials.key; + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + e.printStackTrace(); + } + + return key; + } + + public void setSalt(byte[] bytes) { + String encodedSalt = Base64.encodeToString(bytes, Base64.URL_SAFE); + setString(R.string.settings_key_auth_salt, encodedSalt); + } + + public byte[] getSalt() { + String storedSalt = getString(R.string.settings_key_auth_salt, ""); + + if (storedSalt.isEmpty()) { + byte[] newSalt = EncryptionHelper.generateRandom(Constants.PBKDF2_SALT_LENGTH); + setSalt(newSalt); + + return newSalt; + } else { + return Base64.decode(storedSalt, Base64.URL_SAFE); + } + } + + public int getIterations() { + return getIntValue(R.string.settings_key_auth_iterations, Constants.PBKDF2_DEFAULT_ITERATIONS); + } + + public void setIterations(int value) { + setInt(R.string.settings_key_auth_iterations, value); + } + + public EncryptionType getEncryption() { + String encType = getString(R.string.settings_key_encryption, R.string.settings_default_encryption); + return EncryptionType.valueOf(encType.toUpperCase()); + } + + public void setEncryption(String encryption) { + setString(R.string.settings_key_encryption, encryption); } public Set getPanicResponse() { @@ -281,7 +335,7 @@ public class Settings { } public String getBackupDir() { - return getString(R.string.settings_key_backup_directory, DEFAULT_BACKUP_FOLDER); + return getString(R.string.settings_key_backup_directory, Constants.BACKUP_FOLDER); } public String getBackupPassword() { @@ -295,7 +349,7 @@ public class Settings { String password = ""; try { - KeyPair key = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, KEY_ALIAS); + KeyPair key = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, Constants.KEYSTORE_ALIAS_PASSWORD); password = new String(EncryptionHelper.decrypt(key.getPrivate(), encPassword), StandardCharsets.UTF_8); } catch (Exception e) { e.printStackTrace(); diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/UIHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/UIHelper.java new file mode 100644 index 00000000..89fa0407 --- /dev/null +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/UIHelper.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2017 Jakob Nixdorf + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.shadowice.flocke.andotp.Utilities; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +public class UIHelper { + public static void showGenericDialog(Context context, int titleId, int messageId) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(titleId) + .setMessage(messageId) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + } + }) + .create() + .show(); + } + + public static void showKeyboard(Context context, View view) { + if (view != null) { + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(view, 0); + } + } + + public static void hideKeyboard(Context context, View view) { + if (view != null) { + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + } +} diff --git a/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java b/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java index e7ccda64..a04c308c 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java @@ -62,7 +62,9 @@ import java.util.HashSet; import java.util.List; import java.util.concurrent.Callable; -import static org.shadowice.flocke.andotp.Utilities.Settings.SortMode; +import javax.crypto.SecretKey; + +import static org.shadowice.flocke.andotp.Utilities.Constants.SortMode; public class EntriesCardAdapter extends RecyclerView.Adapter implements ItemTouchHelperAdapter, Filterable { @@ -74,6 +76,8 @@ public class EntriesCardAdapter extends RecyclerView.Adapter private Callback callback; private List tagsFilter = new ArrayList<>(); + private SecretKey encryptionKey = null; + private SortMode sortMode = SortMode.UNSORTED; private TagsAdapter tagsFilterAdapter; private Settings settings; @@ -83,8 +87,15 @@ public class EntriesCardAdapter extends RecyclerView.Adapter this.tagsFilterAdapter = tagsFilterAdapter; this.settings = new Settings(context); this.taskHandler = new Handler(); + this.entries = new ArrayList<>(); + } - loadEntries(); + public void setEncryptionKey(SecretKey key) { + encryptionKey = key; + } + + public SecretKey getEncryptionKey() { + return encryptionKey; } @Override @@ -92,6 +103,10 @@ public class EntriesCardAdapter extends RecyclerView.Adapter return displayedEntries.size(); } + public ArrayList getEntries() { + return entries; + } + public void addEntry(Entry e) { if (! entries.contains(e)) { entries.add(e); @@ -112,12 +127,14 @@ public class EntriesCardAdapter extends RecyclerView.Adapter } public void saveEntries() { - DatabaseHelper.saveDatabase(context, entries); + DatabaseHelper.saveDatabase(context, entries, encryptionKey); } public void loadEntries() { - entries = DatabaseHelper.loadDatabase(context); - entriesChanged(); + if (encryptionKey != null) { + entries = DatabaseHelper.loadDatabase(context, encryptionKey); + entriesChanged(); + } } public void filterByTags(List tags) { @@ -255,7 +272,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter displayedEntries.get(position).setLastUsed(timeStamp); entries.get(realIndex).setLastUsed(timeStamp); - DatabaseHelper.saveDatabase(context, entries); + DatabaseHelper.saveDatabase(context, entries, encryptionKey); if (sortMode == SortMode.LAST_USED) { displayedEntries = sortEntries(displayedEntries); @@ -274,7 +291,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter displayedEntries = new ArrayList<>(entries); notifyItemMoved(fromPosition, toPosition); - DatabaseHelper.saveDatabase(context, entries); + DatabaseHelper.saveDatabase(context, entries, encryptionKey); } return true; @@ -314,7 +331,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter Entry e = entries.get(realIndex); e.setLabel(newLabel); - DatabaseHelper.saveDatabase(context, entries); + DatabaseHelper.saveDatabase(context, entries, encryptionKey); } }) .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @@ -395,7 +412,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter Entry e = entries.get(realIndex); e.setThumbnail(thumbnail); - DatabaseHelper.saveDatabase(context, entries); + DatabaseHelper.saveDatabase(context, entries, encryptionKey); notifyItemChanged(pos); alert.cancel(); } @@ -423,7 +440,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter @Override public Object call() throws Exception { entries.get(realPos).setTags(tagsAdapter.getActiveTags()); - DatabaseHelper.saveDatabase(context, entries); + DatabaseHelper.saveDatabase(context, entries, encryptionKey); List inUseTags = getTags(); @@ -468,7 +485,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter notifyItemRemoved(pos); entries.remove(realIndex); - DatabaseHelper.saveDatabase(context, entries); + DatabaseHelper.saveDatabase(context, entries, encryptionKey); } }) .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { diff --git a/app/src/main/java/org/shadowice/flocke/andotp/View/EntryViewHolder.java b/app/src/main/java/org/shadowice/flocke/andotp/View/EntryViewHolder.java index 68441f87..1638dc6d 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/View/EntryViewHolder.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/View/EntryViewHolder.java @@ -43,7 +43,6 @@ import java.util.List; public class EntryViewHolder extends RecyclerView.ViewHolder implements ItemTouchHelperViewHolder { - private Context context; private Callback callback; private boolean tapToReveal; diff --git a/app/src/main/res/layout/component_authentication.xml b/app/src/main/res/layout/component_authentication.xml new file mode 100644 index 00000000..0ad625f5 --- /dev/null +++ b/app/src/main/res/layout/component_authentication.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + +