Merge branch 'master' into google-backups
# Conflicts: # app/src/main/java/org/shadowice/flocke/andotp/Activities/SettingsActivity.java # app/src/main/java/org/shadowice/flocke/andotp/Utilities/DatabaseHelper.java # app/src/main/res/values/settings.xml # app/src/main/res/values/strings_settings.xml # app/src/main/res/xml/preferences_special.xml
This commit is contained in:
commit
41ca299af6
39 changed files with 1537 additions and 599 deletions
|
@ -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)
|
* **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**:
|
* **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)
|
- [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:
|
#### Contributors:
|
||||||
|
|
||||||
|
|
|
@ -32,8 +32,10 @@ import org.apache.commons.codec.binary.Hex;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
import org.shadowice.flocke.andotp.Database.Entry;
|
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.DatabaseHelper;
|
||||||
import org.shadowice.flocke.andotp.Utilities.EncryptionHelper;
|
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.Utilities.TokenCalculator;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -52,11 +54,10 @@ import java.util.Arrays;
|
||||||
import javax.crypto.BadPaddingException;
|
import javax.crypto.BadPaddingException;
|
||||||
import javax.crypto.IllegalBlockSizeException;
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
import javax.crypto.NoSuchPaddingException;
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
import static org.shadowice.flocke.andotp.Utilities.TokenCalculator.TOTP_DEFAULT_PERIOD;
|
|
||||||
|
|
||||||
public class ApplicationTest extends ApplicationTestCase<Application> {
|
public class ApplicationTest extends ApplicationTestCase<Application> {
|
||||||
|
|
||||||
public ApplicationTest() {
|
public ApplicationTest() {
|
||||||
|
@ -69,29 +70,29 @@ public class ApplicationTest extends ApplicationTestCase<Application> {
|
||||||
byte[] keySHA256 = "12345678901234567890123456789012".getBytes(StandardCharsets.US_ASCII);
|
byte[] keySHA256 = "12345678901234567890123456789012".getBytes(StandardCharsets.US_ASCII);
|
||||||
byte[] keySHA512 = "1234567890123456789012345678901234567890123456789012345678901234".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(94287082, TokenCalculator.TOTP_RFC6238(keySHA1, TokenCalculator.TOTP_DEFAULT_PERIOD, 59L, 8, TokenCalculator.HashAlgorithm.SHA1));
|
||||||
assertEquals(46119246, TokenCalculator.TOTP_RFC6238(keySHA256, TOTP_DEFAULT_PERIOD, 59L, 8, TokenCalculator.HashAlgorithm.SHA256));
|
assertEquals(46119246, TokenCalculator.TOTP_RFC6238(keySHA256, TokenCalculator.TOTP_DEFAULT_PERIOD, 59L, 8, TokenCalculator.HashAlgorithm.SHA256));
|
||||||
assertEquals(90693936, TokenCalculator.TOTP_RFC6238(keySHA512, TOTP_DEFAULT_PERIOD, 59L, 8, TokenCalculator.HashAlgorithm.SHA512));
|
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(7081804, TokenCalculator.TOTP_RFC6238(keySHA1, TokenCalculator.TOTP_DEFAULT_PERIOD, 1111111109L, 8, TokenCalculator.HashAlgorithm.SHA1));
|
||||||
assertEquals(68084774, TokenCalculator.TOTP_RFC6238(keySHA256, TOTP_DEFAULT_PERIOD, 1111111109L, 8, TokenCalculator.HashAlgorithm.SHA256));
|
assertEquals(68084774, TokenCalculator.TOTP_RFC6238(keySHA256, TokenCalculator.TOTP_DEFAULT_PERIOD, 1111111109L, 8, TokenCalculator.HashAlgorithm.SHA256));
|
||||||
assertEquals(25091201, TokenCalculator.TOTP_RFC6238(keySHA512, TOTP_DEFAULT_PERIOD, 1111111109L, 8, TokenCalculator.HashAlgorithm.SHA512));
|
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(14050471, TokenCalculator.TOTP_RFC6238(keySHA1, TokenCalculator.TOTP_DEFAULT_PERIOD, 1111111111L, 8, TokenCalculator.HashAlgorithm.SHA1));
|
||||||
assertEquals(67062674, TokenCalculator.TOTP_RFC6238(keySHA256, TOTP_DEFAULT_PERIOD, 1111111111L, 8, TokenCalculator.HashAlgorithm.SHA256));
|
assertEquals(67062674, TokenCalculator.TOTP_RFC6238(keySHA256, TokenCalculator.TOTP_DEFAULT_PERIOD, 1111111111L, 8, TokenCalculator.HashAlgorithm.SHA256));
|
||||||
assertEquals(99943326, TokenCalculator.TOTP_RFC6238(keySHA512, TOTP_DEFAULT_PERIOD, 1111111111L, 8, TokenCalculator.HashAlgorithm.SHA512));
|
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(89005924, TokenCalculator.TOTP_RFC6238(keySHA1, TokenCalculator.TOTP_DEFAULT_PERIOD, 1234567890L, 8, TokenCalculator.HashAlgorithm.SHA1));
|
||||||
assertEquals(91819424, TokenCalculator.TOTP_RFC6238(keySHA256, TOTP_DEFAULT_PERIOD, 1234567890L, 8, TokenCalculator.HashAlgorithm.SHA256));
|
assertEquals(91819424, TokenCalculator.TOTP_RFC6238(keySHA256, TokenCalculator.TOTP_DEFAULT_PERIOD, 1234567890L, 8, TokenCalculator.HashAlgorithm.SHA256));
|
||||||
assertEquals(93441116, TokenCalculator.TOTP_RFC6238(keySHA512, TOTP_DEFAULT_PERIOD, 1234567890L, 8, TokenCalculator.HashAlgorithm.SHA512));
|
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(69279037, TokenCalculator.TOTP_RFC6238(keySHA1, TokenCalculator.TOTP_DEFAULT_PERIOD, 2000000000L, 8, TokenCalculator.HashAlgorithm.SHA1));
|
||||||
assertEquals(90698825, TokenCalculator.TOTP_RFC6238(keySHA256, TOTP_DEFAULT_PERIOD, 2000000000L, 8, TokenCalculator.HashAlgorithm.SHA256));
|
assertEquals(90698825, TokenCalculator.TOTP_RFC6238(keySHA256, TokenCalculator.TOTP_DEFAULT_PERIOD, 2000000000L, 8, TokenCalculator.HashAlgorithm.SHA256));
|
||||||
assertEquals(38618901, TokenCalculator.TOTP_RFC6238(keySHA512, TOTP_DEFAULT_PERIOD, 2000000000L, 8, TokenCalculator.HashAlgorithm.SHA512));
|
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(65353130, TokenCalculator.TOTP_RFC6238(keySHA1, TokenCalculator.TOTP_DEFAULT_PERIOD, 20000000000L, 8, TokenCalculator.HashAlgorithm.SHA1));
|
||||||
assertEquals(77737706, TokenCalculator.TOTP_RFC6238(keySHA256, TOTP_DEFAULT_PERIOD, 20000000000L, 8, TokenCalculator.HashAlgorithm.SHA256));
|
assertEquals(77737706, TokenCalculator.TOTP_RFC6238(keySHA256, TokenCalculator.TOTP_DEFAULT_PERIOD, 20000000000L, 8, TokenCalculator.HashAlgorithm.SHA256));
|
||||||
assertEquals(47863826, TokenCalculator.TOTP_RFC6238(keySHA512, TOTP_DEFAULT_PERIOD, 20000000000L, 8, TokenCalculator.HashAlgorithm.SHA512));
|
assertEquals(47863826, TokenCalculator.TOTP_RFC6238(keySHA512, TokenCalculator.TOTP_DEFAULT_PERIOD, 20000000000L, 8, TokenCalculator.HashAlgorithm.SHA512));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -106,6 +107,8 @@ public class ApplicationTest extends ApplicationTestCase<Application> {
|
||||||
"\"digits\":6," +
|
"\"digits\":6," +
|
||||||
"\"type\":\"TOTP\"," +
|
"\"type\":\"TOTP\"," +
|
||||||
"\"algorithm\":\"SHA1\"," +
|
"\"algorithm\":\"SHA1\"," +
|
||||||
|
"\"thumbnail\":\"Default\"," +
|
||||||
|
"\"last_used\":0," +
|
||||||
"\"tags\":[\"test1\",\"test2\"]}";
|
"\"tags\":[\"test1\",\"test2\"]}";
|
||||||
|
|
||||||
Entry e = new Entry(new JSONObject(s));
|
Entry e = new Entry(new JSONObject(s));
|
||||||
|
@ -170,10 +173,11 @@ public class ApplicationTest extends ApplicationTestCase<Application> {
|
||||||
keyStore.load(null);
|
keyStore.load(null);
|
||||||
keyStore.deleteEntry("settings");
|
keyStore.deleteEntry("settings");
|
||||||
|
|
||||||
new File(context.getFilesDir() + "/" + DatabaseHelper.SETTINGS_FILE).delete();
|
new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE).delete();
|
||||||
new File(context.getFilesDir() + "/" + DatabaseHelper.KEY_FILE).delete();
|
new File(context.getFilesDir() + "/" + Constants.FILENAME_ENCRYPTED_KEY).delete();
|
||||||
|
|
||||||
ArrayList<Entry> b = DatabaseHelper.loadDatabase(context);
|
SecretKey encryptionKey = KeyStoreHelper.loadEncryptionKeyFromKeyStore(context);
|
||||||
|
ArrayList<Entry> b = DatabaseHelper.loadDatabase(context, encryptionKey);
|
||||||
assertEquals(0, b.size());
|
assertEquals(0, b.size());
|
||||||
|
|
||||||
ArrayList<Entry> a = new ArrayList<>();
|
ArrayList<Entry> a = new ArrayList<>();
|
||||||
|
@ -187,13 +191,13 @@ public class ApplicationTest extends ApplicationTestCase<Application> {
|
||||||
e.setSecret("secret2".getBytes());
|
e.setSecret("secret2".getBytes());
|
||||||
a.add(e);
|
a.add(e);
|
||||||
|
|
||||||
DatabaseHelper.saveDatabase(context, a);
|
DatabaseHelper.saveDatabase(context, a, encryptionKey);
|
||||||
b = DatabaseHelper.loadDatabase(context);
|
b = DatabaseHelper.loadDatabase(context, encryptionKey);
|
||||||
|
|
||||||
assertEquals(a, b);
|
assertEquals(a, b);
|
||||||
|
|
||||||
new File(context.getFilesDir() + "/" + DatabaseHelper.SETTINGS_FILE).delete();
|
new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE).delete();
|
||||||
new File(context.getFilesDir() + "/" + DatabaseHelper.KEY_FILE).delete();
|
new File(context.getFilesDir() + "/" + Constants.FILENAME_ENCRYPTED_KEY).delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testEncryptionHelper() throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, UnsupportedEncodingException, InvalidAlgorithmParameterException, DecoderException {
|
public void testEncryptionHelper() throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, UnsupportedEncodingException, InvalidAlgorithmParameterException, DecoderException {
|
||||||
|
|
|
@ -29,11 +29,13 @@ import android.support.design.widget.TextInputLayout;
|
||||||
import android.support.v7.widget.Toolbar;
|
import android.support.v7.widget.Toolbar;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
import android.text.method.PasswordTransformationMethod;
|
import android.text.method.PasswordTransformationMethod;
|
||||||
|
import android.util.Base64;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewStub;
|
import android.view.ViewStub;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
import android.widget.Button;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
@ -41,13 +43,25 @@ import android.widget.Toast;
|
||||||
import org.apache.commons.codec.binary.Hex;
|
import org.apache.commons.codec.binary.Hex;
|
||||||
import org.apache.commons.codec.digest.DigestUtils;
|
import org.apache.commons.codec.digest.DigestUtils;
|
||||||
import org.shadowice.flocke.andotp.R;
|
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
|
public class AuthenticateActivity extends ThemedActivity
|
||||||
implements EditText.OnEditorActionListener {
|
implements EditText.OnEditorActionListener, View.OnClickListener {
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
|
AuthMethod authMethod;
|
||||||
|
String newEncryption = "";
|
||||||
|
boolean oldPassword = false;
|
||||||
|
|
||||||
|
TextInputEditText passwordInput;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
@ -63,65 +77,114 @@ public class AuthenticateActivity extends ThemedActivity
|
||||||
stub.setLayoutResource(R.layout.content_authenticate);
|
stub.setLayoutResource(R.layout.content_authenticate);
|
||||||
View v = stub.inflate();
|
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);
|
TextView passwordLabel = v.findViewById(R.id.passwordLabel);
|
||||||
TextInputLayout passwordLayout = v.findViewById(R.id.passwordLayout);
|
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);
|
||||||
|
|
||||||
if (authMethod == AuthMethod.PASSWORD) {
|
authMethod = settings.getAuthMethod();
|
||||||
password = settings.getAuthPasswordHash();
|
password = settings.getAuthCredentials();
|
||||||
|
|
||||||
|
if (password.isEmpty()) {
|
||||||
|
password = settings.getOldCredentials(authMethod);
|
||||||
|
oldPassword = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authMethod == AuthMethod.PASSWORD) {
|
||||||
if (password.isEmpty()) {
|
if (password.isEmpty()) {
|
||||||
Toast.makeText(this, R.string.auth_toast_password_missing, Toast.LENGTH_LONG).show();
|
Toast.makeText(this, R.string.auth_toast_password_missing, Toast.LENGTH_LONG).show();
|
||||||
finishWithResult(true);
|
finishWithResult(true, null);
|
||||||
} else {
|
} else {
|
||||||
passwordLabel.setText(R.string.auth_msg_password);
|
|
||||||
passwordLayout.setHint(getString(R.string.auth_hint_password));
|
passwordLayout.setHint(getString(R.string.auth_hint_password));
|
||||||
passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||||
}
|
}
|
||||||
} else if (authMethod == AuthMethod.PIN) {
|
} else if (authMethod == AuthMethod.PIN) {
|
||||||
password = settings.getAuthPINHash();
|
|
||||||
|
|
||||||
if (password.isEmpty()) {
|
if (password.isEmpty()) {
|
||||||
Toast.makeText(this, R.string.auth_toast_pin_missing, Toast.LENGTH_LONG).show();
|
Toast.makeText(this, R.string.auth_toast_pin_missing, Toast.LENGTH_LONG).show();
|
||||||
finishWithResult(true);
|
finishWithResult(true, null);
|
||||||
} else {
|
} else {
|
||||||
passwordLabel.setText(R.string.auth_msg_pin);
|
|
||||||
passwordLayout.setHint(getString(R.string.auth_hint_pin));
|
passwordLayout.setHint(getString(R.string.auth_hint_pin));
|
||||||
passwordInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
|
passwordInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
finishWithResult(true);
|
finishWithResult(true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
passwordInput.setTransformationMethod(new PasswordTransformationMethod());
|
passwordInput.setTransformationMethod(new PasswordTransformationMethod());
|
||||||
passwordInput.setOnEditorActionListener(this);
|
passwordInput.setOnEditorActionListener(this);
|
||||||
|
|
||||||
|
Button unlockButton = v.findViewById(R.id.buttonUnlock);
|
||||||
|
unlockButton.setOnClickListener(this);
|
||||||
|
|
||||||
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
|
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
checkPassword(passwordInput.getText().toString());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||||
String hashedPassword = new String(Hex.encodeHex(DigestUtils.sha256(v.getText().toString())));
|
checkPassword(v.getText().toString());
|
||||||
|
|
||||||
if (hashedPassword.equals(password)) {
|
|
||||||
finishWithResult(true);
|
|
||||||
} else {
|
|
||||||
finishWithResult(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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
|
// End with a result
|
||||||
public void finishWithResult(boolean success) {
|
public void finishWithResult(boolean success, byte[] key) {
|
||||||
Intent data = new Intent();
|
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)
|
if (success)
|
||||||
setResult(RESULT_OK, data);
|
setResult(RESULT_OK, data);
|
||||||
|
|
||||||
|
@ -131,7 +194,7 @@ public class AuthenticateActivity extends ThemedActivity
|
||||||
// Go back to the main activity
|
// Go back to the main activity
|
||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
finishWithResult(false);
|
finishWithResult(false, null);
|
||||||
super.onBackPressed();
|
super.onBackPressed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,7 @@ import org.openintents.openpgp.util.OpenPgpApi;
|
||||||
import org.openintents.openpgp.util.OpenPgpServiceConnection;
|
import org.openintents.openpgp.util.OpenPgpServiceConnection;
|
||||||
import org.shadowice.flocke.andotp.Database.Entry;
|
import org.shadowice.flocke.andotp.Database.Entry;
|
||||||
import org.shadowice.flocke.andotp.R;
|
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.DatabaseHelper;
|
||||||
import org.shadowice.flocke.andotp.Utilities.EncryptionHelper;
|
import org.shadowice.flocke.andotp.Utilities.EncryptionHelper;
|
||||||
import org.shadowice.flocke.andotp.Utilities.FileHelper;
|
import org.shadowice.flocke.andotp.Utilities.FileHelper;
|
||||||
|
@ -64,28 +65,7 @@ import java.util.ArrayList;
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
public class BackupActivity extends BaseActivity {
|
public class BackupActivity extends BaseActivity {
|
||||||
private final static int INTENT_OPEN_DOCUMENT_PLAIN = 200;
|
private SecretKey encryptionKey = null;
|
||||||
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 OpenPgpServiceConnection pgpServiceConnection;
|
private OpenPgpServiceConnection pgpServiceConnection;
|
||||||
private long pgpKeyId;
|
private long pgpKeyId;
|
||||||
|
@ -111,6 +91,10 @@ public class BackupActivity extends BaseActivity {
|
||||||
stub.setLayoutResource(R.layout.content_backup);
|
stub.setLayoutResource(R.layout.content_backup);
|
||||||
View v = stub.inflate();
|
View v = stub.inflate();
|
||||||
|
|
||||||
|
Intent callingIntent = getIntent();
|
||||||
|
byte[] keyMaterial = callingIntent.getByteArrayExtra(Constants.EXTRA_BACKUP_ENCRYPTION_KEY);
|
||||||
|
encryptionKey = EncryptionHelper.generateSymmetricKey(keyMaterial);
|
||||||
|
|
||||||
// Plain-text
|
// Plain-text
|
||||||
|
|
||||||
LinearLayout backupPlain = v.findViewById(R.id.button_backup_plain);
|
LinearLayout backupPlain = v.findViewById(R.id.button_backup_plain);
|
||||||
|
@ -126,7 +110,7 @@ public class BackupActivity extends BaseActivity {
|
||||||
restorePlain.setOnClickListener(new View.OnClickListener() {
|
restorePlain.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
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() {
|
backupCrypt.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
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() {
|
restoreCrypt.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
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() {
|
backupPGP.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
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() {
|
restorePGP.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
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
|
// Get the result from permission requests
|
||||||
@Override
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
|
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) {
|
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
showOpenFileSelector(INTENT_OPEN_DOCUMENT_PLAIN);
|
showOpenFileSelector(Constants.INTENT_BACKUP_OPEN_DOCUMENT_PLAIN);
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this, R.string.backup_toast_storage_permissions, Toast.LENGTH_LONG).show();
|
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) {
|
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 {
|
} else {
|
||||||
Toast.makeText(this, R.string.backup_toast_storage_permissions, Toast.LENGTH_LONG).show();
|
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) {
|
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
showOpenFileSelector(INTENT_OPEN_DOCUMENT_CRYPT);
|
showOpenFileSelector(Constants.INTENT_BACKUP_OPEN_DOCUMENT_CRYPT);
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this, R.string.backup_toast_storage_permissions, Toast.LENGTH_LONG).show();
|
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) {
|
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 {
|
} else {
|
||||||
Toast.makeText(this, R.string.backup_toast_storage_permissions, Toast.LENGTH_LONG).show();
|
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) {
|
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
showOpenFileSelector(INTENT_OPEN_DOCUMENT_PGP);
|
showOpenFileSelector(Constants.INTENT_BACKUP_OPEN_DOCUMENT_PGP);
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this, R.string.backup_toast_storage_permissions, Toast.LENGTH_LONG).show();
|
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) {
|
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 {
|
} else {
|
||||||
Toast.makeText(this, R.string.backup_toast_storage_permissions, Toast.LENGTH_LONG).show();
|
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) {
|
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
|
||||||
super.onActivityResult(requestCode, resultCode, 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) {
|
if (intent != null) {
|
||||||
doRestorePlain(intent.getData());
|
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) {
|
if (intent != null) {
|
||||||
doBackupPlain(intent.getData());
|
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) {
|
if (intent != null) {
|
||||||
doRestoreCrypt(intent.getData());
|
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) {
|
if (intent != null) {
|
||||||
doBackupCrypt(intent.getData());
|
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)
|
if (intent != null)
|
||||||
restoreEncryptedWithPGP(intent.getData(), 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)
|
if (intent != null)
|
||||||
backupEncryptedWithPGP(intent.getData(), 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);
|
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);
|
restoreEncryptedWithPGP(decryptSourceFile, intent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -316,12 +300,12 @@ public class BackupActivity extends BaseActivity {
|
||||||
intent.setType("*/*");
|
intent.setType("*/*");
|
||||||
startActivityForResult(intent, intentId);
|
startActivityForResult(intent, intentId);
|
||||||
} else {
|
} else {
|
||||||
if (intentId == INTENT_OPEN_DOCUMENT_PLAIN)
|
if (intentId == Constants.INTENT_BACKUP_OPEN_DOCUMENT_PLAIN)
|
||||||
doRestorePlain(Tools.buildUri(settings.getBackupDir(), DEFAULT_BACKUP_FILENAME_PLAIN));
|
doRestorePlain(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_PLAIN));
|
||||||
else if (intentId == INTENT_OPEN_DOCUMENT_CRYPT)
|
else if (intentId == Constants.INTENT_BACKUP_OPEN_DOCUMENT_CRYPT)
|
||||||
doRestoreCrypt(Tools.buildUri(settings.getBackupDir(), DEFAULT_BACKUP_FILENAME_CRYPT));
|
doRestoreCrypt(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_CRYPT));
|
||||||
else if (intentId == INTENT_OPEN_DOCUMENT_PGP)
|
else if (intentId == Constants.INTENT_BACKUP_OPEN_DOCUMENT_PGP)
|
||||||
restoreEncryptedWithPGP(Tools.buildUri(settings.getBackupDir(), DEFAULT_BACKUP_FILENAME_PGP), null);
|
restoreEncryptedWithPGP(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_PGP), null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -334,12 +318,12 @@ public class BackupActivity extends BaseActivity {
|
||||||
startActivityForResult(intent, intentId);
|
startActivityForResult(intent, intentId);
|
||||||
} else {
|
} else {
|
||||||
if (Tools.mkdir(settings.getBackupDir())) {
|
if (Tools.mkdir(settings.getBackupDir())) {
|
||||||
if (intentId == INTENT_SAVE_DOCUMENT_PLAIN)
|
if (intentId == Constants.INTENT_BACKUP_SAVE_DOCUMENT_PLAIN)
|
||||||
doBackupPlain(Tools.buildUri(settings.getBackupDir(), DEFAULT_BACKUP_FILENAME_PLAIN));
|
doBackupPlain(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_PLAIN));
|
||||||
else if (intentId == INTENT_SAVE_DOCUMENT_CRYPT)
|
else if (intentId == Constants.INTENT_BACKUP_SAVE_DOCUMENT_CRYPT)
|
||||||
doBackupCrypt(Tools.buildUri(settings.getBackupDir(), DEFAULT_BACKUP_FILENAME_CRYPT));
|
doBackupCrypt(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_CRYPT));
|
||||||
else if (intentId == INTENT_SAVE_DOCUMENT_PGP)
|
else if (intentId == Constants.INTENT_BACKUP_SAVE_DOCUMENT_PGP)
|
||||||
backupEncryptedWithPGP(Tools.buildUri(settings.getBackupDir(), DEFAULT_BACKUP_FILENAME_PGP), null);
|
backupEncryptedWithPGP(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_PGP), null);
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this, R.string.backup_toast_mkdir_failed, Toast.LENGTH_LONG).show();
|
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 (entries.size() > 0) {
|
||||||
if (! replace.isChecked()) {
|
if (! replace.isChecked()) {
|
||||||
ArrayList<Entry> currentEntries = DatabaseHelper.loadDatabase(this);
|
ArrayList<Entry> currentEntries = DatabaseHelper.loadDatabase(this, encryptionKey);
|
||||||
|
|
||||||
entries.removeAll(currentEntries);
|
entries.removeAll(currentEntries);
|
||||||
entries.addAll(currentEntries);
|
entries.addAll(currentEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DatabaseHelper.saveDatabase(this, entries)) {
|
if (DatabaseHelper.saveDatabase(this, entries, encryptionKey)) {
|
||||||
reload = true;
|
reload = true;
|
||||||
Toast.makeText(this, R.string.backup_toast_import_success, Toast.LENGTH_LONG).show();
|
Toast.makeText(this, R.string.backup_toast_import_success, Toast.LENGTH_LONG).show();
|
||||||
finishWithResult();
|
finishWithResult();
|
||||||
|
@ -399,9 +383,9 @@ public class BackupActivity extends BaseActivity {
|
||||||
|
|
||||||
private void doBackupPlain(Uri uri) {
|
private void doBackupPlain(Uri uri) {
|
||||||
if (Tools.isExternalStorageWritable()) {
|
if (Tools.isExternalStorageWritable()) {
|
||||||
boolean success = DatabaseHelper.exportAsJSON(this, uri);
|
ArrayList<Entry> 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();
|
Toast.makeText(this, R.string.backup_toast_export_success, Toast.LENGTH_LONG).show();
|
||||||
else
|
else
|
||||||
Toast.makeText(this, R.string.backup_toast_export_failed, Toast.LENGTH_LONG).show();
|
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() {
|
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(DialogInterface dialogInterface, int i) {
|
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() {
|
.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
|
||||||
|
@ -472,7 +456,7 @@ public class BackupActivity extends BaseActivity {
|
||||||
|
|
||||||
if (! password.isEmpty()) {
|
if (! password.isEmpty()) {
|
||||||
if (Tools.isExternalStorageWritable()) {
|
if (Tools.isExternalStorageWritable()) {
|
||||||
ArrayList<Entry> entries = DatabaseHelper.loadDatabase(this);
|
ArrayList<Entry> entries = DatabaseHelper.loadDatabase(this, encryptionKey);
|
||||||
String plain = DatabaseHelper.entriesToString(entries);
|
String plain = DatabaseHelper.entriesToString(entries);
|
||||||
|
|
||||||
boolean success = true;
|
boolean success = true;
|
||||||
|
@ -515,7 +499,7 @@ public class BackupActivity extends BaseActivity {
|
||||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
OpenPgpApi api = new OpenPgpApi(this, pgpServiceConnection.getService());
|
OpenPgpApi api = new OpenPgpApi(this, pgpServiceConnection.getService());
|
||||||
Intent result = api.executeApi(decryptIntent, is, os);
|
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) {
|
private void doBackupEncrypted(Uri uri, String data) {
|
||||||
|
@ -534,7 +518,7 @@ public class BackupActivity extends BaseActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void backupEncryptedWithPGP(Uri uri, Intent encryptIntent) {
|
private void backupEncryptedWithPGP(Uri uri, Intent encryptIntent) {
|
||||||
ArrayList<Entry> entries = DatabaseHelper.loadDatabase(this);
|
ArrayList<Entry> entries = DatabaseHelper.loadDatabase(this, encryptionKey);
|
||||||
String plainJSON = DatabaseHelper.entriesToString(entries);
|
String plainJSON = DatabaseHelper.entriesToString(entries);
|
||||||
|
|
||||||
if (encryptIntent == null) {
|
if (encryptIntent == null) {
|
||||||
|
@ -555,7 +539,7 @@ public class BackupActivity extends BaseActivity {
|
||||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
OpenPgpApi api = new OpenPgpApi(this, pgpServiceConnection.getService());
|
OpenPgpApi api = new OpenPgpApi(this, pgpServiceConnection.getService());
|
||||||
Intent result = api.executeApi(encryptIntent, is, os);
|
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) {
|
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) {
|
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 (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)
|
if (os != null)
|
||||||
doBackupEncrypted(file, outputStreamToString(os));
|
doBackupEncrypted(file, outputStreamToString(os));
|
||||||
} else if (requestCode == INTENT_DECRYPT_PGP) {
|
} else if (requestCode == Constants.INTENT_BACKUP_DECRYPT_PGP) {
|
||||||
if (os != null) {
|
if (os != null) {
|
||||||
if (settings.getOpenPGPVerify()) {
|
if (settings.getOpenPGPVerify()) {
|
||||||
OpenPgpSignatureResult sigResult = result.getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE);
|
OpenPgpSignatureResult sigResult = result.getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE);
|
||||||
|
@ -586,9 +570,9 @@ public class BackupActivity extends BaseActivity {
|
||||||
PendingIntent pi = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
|
PendingIntent pi = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
|
||||||
|
|
||||||
// Small hack to keep the target file even after user interaction
|
// 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;
|
encryptTargetFile = file;
|
||||||
} else if (requestCode == INTENT_DECRYPT_PGP) {
|
} else if (requestCode == Constants.INTENT_BACKUP_DECRYPT_PGP) {
|
||||||
decryptSourceFile = file;
|
decryptSourceFile = file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,8 +58,9 @@ import com.google.zxing.integration.android.IntentResult;
|
||||||
|
|
||||||
import org.shadowice.flocke.andotp.Database.Entry;
|
import org.shadowice.flocke.andotp.Database.Entry;
|
||||||
import org.shadowice.flocke.andotp.R;
|
import org.shadowice.flocke.andotp.R;
|
||||||
import org.shadowice.flocke.andotp.Utilities.DatabaseHelper;
|
import org.shadowice.flocke.andotp.Utilities.Constants;
|
||||||
import org.shadowice.flocke.andotp.Utilities.Settings;
|
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.Utilities.TokenCalculator;
|
||||||
import org.shadowice.flocke.andotp.View.EntriesCardAdapter;
|
import org.shadowice.flocke.andotp.View.EntriesCardAdapter;
|
||||||
import org.shadowice.flocke.andotp.View.FloatingActionMenu;
|
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.ArrayList;
|
||||||
import java.util.HashMap;
|
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
|
public class MainActivity extends BaseActivity
|
||||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
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 EntriesCardAdapter adapter;
|
||||||
private FloatingActionMenu floatingActionMenu;
|
private FloatingActionMenu floatingActionMenu;
|
||||||
|
@ -84,6 +86,7 @@ public class MainActivity extends BaseActivity
|
||||||
private MenuItem sortMenu;
|
private MenuItem sortMenu;
|
||||||
private SimpleItemTouchHelperCallback touchHelperCallback;
|
private SimpleItemTouchHelperCallback touchHelperCallback;
|
||||||
|
|
||||||
|
private EncryptionType encryptionType = EncryptionType.KEYSTORE;
|
||||||
private boolean requireAuthentication = false;
|
private boolean requireAuthentication = false;
|
||||||
|
|
||||||
private Handler handler;
|
private Handler handler;
|
||||||
|
@ -103,33 +106,51 @@ public class MainActivity extends BaseActivity
|
||||||
|
|
||||||
private void showFirstTimeWarning() {
|
private void showFirstTimeWarning() {
|
||||||
ViewGroup container = findViewById(R.id.main_content);
|
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);
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||||
builder.setTitle(R.string.dialog_title_security_backup)
|
builder.setTitle(R.string.dialog_title_encryption)
|
||||||
.setView(msgView)
|
.setView(msgView)
|
||||||
.setPositiveButton(R.string.button_warned, new DialogInterface.OnClickListener() {
|
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(DialogInterface dialogInterface, int i) {
|
public void onClick(DialogInterface dialogInterface, int i) {
|
||||||
settings.setFirstTimeWarningShown(true);
|
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()
|
.create()
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void authenticate() {
|
public void authenticate(int messageId) {
|
||||||
Settings.AuthMethod authMethod = settings.getAuthMethod();
|
AuthMethod authMethod = settings.getAuthMethod();
|
||||||
|
|
||||||
if (authMethod == Settings.AuthMethod.DEVICE) {
|
if (authMethod == AuthMethod.DEVICE) {
|
||||||
KeyguardManager km = (KeyguardManager) getSystemService(KEYGUARD_SERVICE);
|
KeyguardManager km = (KeyguardManager) getSystemService(KEYGUARD_SERVICE);
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP && km.isKeyguardSecure()) {
|
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));
|
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);
|
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);
|
settings.setSortMode(mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private HashMap<String, Boolean> createTagsMap(ArrayList<Entry> entries) {
|
||||||
|
HashMap<String, Boolean> 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
|
// Initialize the main application
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
@ -168,12 +206,15 @@ public class MainActivity extends BaseActivity
|
||||||
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
||||||
settings.registerPreferenceChangeListener(this);
|
settings.registerPreferenceChangeListener(this);
|
||||||
|
|
||||||
if (savedInstanceState == null)
|
encryptionType = settings.getEncryption();
|
||||||
|
|
||||||
|
if (settings.getAuthMethod() != AuthMethod.NONE && savedInstanceState == null)
|
||||||
requireAuthentication = true;
|
requireAuthentication = true;
|
||||||
|
|
||||||
setBroadcastCallback(new BroadcastReceivedCallback() {
|
setBroadcastCallback(new BroadcastReceivedCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onReceivedScreenOff() {
|
public void onReceivedScreenOff() {
|
||||||
|
if (settings.getAuthMethod() != AuthMethod.NONE)
|
||||||
requireAuthentication = true;
|
requireAuthentication = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -203,16 +244,10 @@ public class MainActivity extends BaseActivity
|
||||||
llm.setOrientation(LinearLayoutManager.VERTICAL);
|
llm.setOrientation(LinearLayoutManager.VERTICAL);
|
||||||
recList.setLayoutManager(llm);
|
recList.setLayoutManager(llm);
|
||||||
|
|
||||||
HashMap<String, Boolean> tagsHashMap = new HashMap<>();
|
tagsDrawerAdapter = new TagsAdapter(this, new HashMap<String, Boolean>());
|
||||||
for(Entry entry : DatabaseHelper.loadDatabase(this)) {
|
|
||||||
for(String tag : entry.getTags())
|
|
||||||
tagsHashMap.put(tag, settings.getTagToggle(tag));
|
|
||||||
}
|
|
||||||
tagsDrawerAdapter = new TagsAdapter(this, tagsHashMap);
|
|
||||||
|
|
||||||
adapter = new EntriesCardAdapter(this, tagsDrawerAdapter);
|
adapter = new EntriesCardAdapter(this, tagsDrawerAdapter);
|
||||||
recList.setAdapter(adapter);
|
|
||||||
|
|
||||||
|
recList.setAdapter(adapter);
|
||||||
recList.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
recList.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||||
|
@ -293,8 +328,18 @@ public class MainActivity extends BaseActivity
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
if (requireAuthentication) {
|
if (requireAuthentication) {
|
||||||
|
if (settings.getAuthMethod() != AuthMethod.NONE) {
|
||||||
requireAuthentication = false;
|
requireAuthentication = false;
|
||||||
authenticate();
|
authenticate(R.string.auth_msg_authenticate);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (settings.getFirstTimeWarningShown()) {
|
||||||
|
if (adapter.getEncryptionKey() == null) {
|
||||||
|
updateEncryption(null);
|
||||||
|
} else {
|
||||||
|
populateAdapter();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startUpdater();
|
startUpdater();
|
||||||
|
@ -344,14 +389,20 @@ public class MainActivity extends BaseActivity
|
||||||
Toast.makeText(this, R.string.toast_invalid_qr_code, Toast.LENGTH_LONG).show();
|
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)) {
|
if (intent.getBooleanExtra("reload", false)) {
|
||||||
adapter.loadEntries();
|
adapter.loadEntries();
|
||||||
refreshTags();
|
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) {
|
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) {
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||||
finishAndRemoveTask();
|
finishAndRemoveTask();
|
||||||
|
@ -360,10 +411,34 @@ public class MainActivity extends BaseActivity
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
requireAuthentication = false;
|
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
|
// Options menu
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
@ -438,10 +513,13 @@ public class MainActivity extends BaseActivity
|
||||||
|
|
||||||
if (id == R.id.action_backup) {
|
if (id == R.id.action_backup) {
|
||||||
Intent backupIntent = new Intent(this, BackupActivity.class);
|
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) {
|
} else if (id == R.id.action_settings) {
|
||||||
Intent settingsIntent = new Intent(this, SettingsActivity.class);
|
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){
|
} else if (id == R.id.action_about){
|
||||||
Intent aboutIntent = new Intent(this, AboutActivity.class);
|
Intent aboutIntent = new Intent(this, AboutActivity.class);
|
||||||
startActivity(aboutIntent);
|
startActivity(aboutIntent);
|
||||||
|
|
|
@ -27,11 +27,10 @@ import android.content.Intent;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import org.shadowice.flocke.andotp.Database.Entry;
|
|
||||||
import org.shadowice.flocke.andotp.Utilities.DatabaseHelper;
|
import org.shadowice.flocke.andotp.Utilities.DatabaseHelper;
|
||||||
|
import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper;
|
||||||
import org.shadowice.flocke.andotp.Utilities.Settings;
|
import org.shadowice.flocke.andotp.Utilities.Settings;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public class PanicResponderActivity extends Activity {
|
public class PanicResponderActivity extends Activity {
|
||||||
|
@ -47,8 +46,10 @@ public class PanicResponderActivity extends Activity {
|
||||||
|
|
||||||
Set<String> response = settings.getPanicResponse();
|
Set<String> response = settings.getPanicResponse();
|
||||||
|
|
||||||
if (response.contains("accounts"))
|
if (response.contains("accounts")) {
|
||||||
DatabaseHelper.saveDatabase(this, new ArrayList<Entry>());
|
DatabaseHelper.wipeDatabase(this);
|
||||||
|
KeyStoreHelper.wipeKeys(this);
|
||||||
|
}
|
||||||
|
|
||||||
if (response.contains("settings"))
|
if (response.contains("settings"))
|
||||||
settings.clear(true);
|
settings.clear(true);
|
||||||
|
|
|
@ -25,6 +25,8 @@ package org.shadowice.flocke.andotp.Activities;
|
||||||
import android.app.KeyguardManager;
|
import android.app.KeyguardManager;
|
||||||
import android.app.backup.BackupManager;
|
import android.app.backup.BackupManager;
|
||||||
import android.app.backup.RestoreObserver;
|
import android.app.backup.RestoreObserver;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
@ -40,13 +42,30 @@ import android.widget.Toast;
|
||||||
|
|
||||||
import org.openintents.openpgp.util.OpenPgpAppPreference;
|
import org.openintents.openpgp.util.OpenPgpAppPreference;
|
||||||
import org.openintents.openpgp.util.OpenPgpKeyPreference;
|
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.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
|
public class SettingsActivity extends BaseActivity
|
||||||
implements SharedPreferences.OnSharedPreferenceChangeListener{
|
implements SharedPreferences.OnSharedPreferenceChangeListener{
|
||||||
SettingsFragment fragment;
|
SettingsFragment fragment;
|
||||||
|
|
||||||
|
SecretKey encryptionKey = null;
|
||||||
|
boolean encryptionChanged = false;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
@ -60,6 +79,11 @@ public class SettingsActivity extends BaseActivity
|
||||||
ViewStub stub = findViewById(R.id.container_stub);
|
ViewStub stub = findViewById(R.id.container_stub);
|
||||||
stub.inflate();
|
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();
|
fragment = new SettingsFragment();
|
||||||
|
|
||||||
getFragmentManager().beginTransaction()
|
getFragmentManager().beginTransaction()
|
||||||
|
@ -71,7 +95,13 @@ public class SettingsActivity extends BaseActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
public void finishWithResult() {
|
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();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,10 +128,87 @@ 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<Entry> 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
|
@Override
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
super.onActivityResult(requestCode, resultCode, 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
|
// handled by OpenPgpKeyPreference
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -110,80 +217,57 @@ public class SettingsActivity extends BaseActivity
|
||||||
public static class SettingsFragment extends PreferenceFragment {
|
public static class SettingsFragment extends PreferenceFragment {
|
||||||
PreferenceCategory catSecurity;
|
PreferenceCategory catSecurity;
|
||||||
|
|
||||||
|
Settings settings;
|
||||||
|
ListPreference encryption;
|
||||||
|
|
||||||
OpenPgpAppPreference pgpProvider;
|
OpenPgpAppPreference pgpProvider;
|
||||||
OpenPgpKeyPreference pgpKey;
|
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
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(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);
|
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
|
// Authentication
|
||||||
catSecurity = (PreferenceCategory) findPreference(getString(R.string.settings_key_cat_security));
|
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());
|
encryption.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||||
|
|
||||||
authPref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onPreferenceChange(Preference preference, Object o) {
|
public boolean onPreferenceChange(final Preference preference, Object o) {
|
||||||
String newAuth = (String) o;
|
String newEncryption = (String) o;
|
||||||
|
EncryptionType encryptionType = EncryptionType.valueOf(newEncryption.toUpperCase());
|
||||||
|
AuthMethod authMethod = settings.getAuthMethod();
|
||||||
|
|
||||||
if (newAuth.equals("device")) {
|
if (encryptionType == EncryptionType.PASSWORD) {
|
||||||
KeyguardManager km = (KeyguardManager) getActivity().getSystemService(KEYGUARD_SERVICE);
|
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);
|
||||||
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();
|
|
||||||
return false;
|
return false;
|
||||||
} else if (! km.isKeyguardSecure()) {
|
} else {
|
||||||
Toast.makeText(getActivity(), R.string.settings_toast_auth_device_not_secure, Toast.LENGTH_LONG).show();
|
if (settings.getAuthCredentials().isEmpty()) {
|
||||||
|
UIHelper.showGenericDialog(getActivity(), R.string.settings_dialog_title_error, R.string.settings_dialog_msg_encryption_invalid_without_credentials);
|
||||||
return false;
|
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)) &&
|
if (sharedPref.contains(getString(R.string.settings_key_special_features)) &&
|
||||||
sharedPref.getBoolean(getString(R.string.settings_key_special_features), false)) {
|
sharedPref.getBoolean(getString(R.string.settings_key_special_features), false)) {
|
||||||
addPreferencesFromResource(R.xml.preferences_special);
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,9 @@ import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public class Entry {
|
public class Entry {
|
||||||
public enum OTPType { TOTP, STEAM}
|
public enum OTPType {
|
||||||
|
TOTP, STEAM
|
||||||
|
}
|
||||||
public static Set<OTPType> PublicTypes = EnumSet.of(OTPType.TOTP);
|
public static Set<OTPType> PublicTypes = EnumSet.of(OTPType.TOTP);
|
||||||
|
|
||||||
private static final OTPType DEFAULT_TYPE = OTPType.TOTP;
|
private static final OTPType DEFAULT_TYPE = OTPType.TOTP;
|
||||||
|
|
|
@ -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<String> entries;
|
||||||
|
private static final List<AuthMethod> 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<String> 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) {}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -39,6 +39,7 @@ import android.widget.Button;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
|
|
||||||
import org.shadowice.flocke.andotp.R;
|
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.EncryptionHelper;
|
||||||
import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper;
|
import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper;
|
||||||
|
|
||||||
|
@ -52,7 +53,6 @@ public class PasswordEncryptedPreference extends DialogPreference
|
||||||
PASSWORD, PIN
|
PASSWORD, PIN
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final String KEY_ALIAS = "password";
|
|
||||||
private KeyPair key;
|
private KeyPair key;
|
||||||
|
|
||||||
private static final String DEFAULT_VALUE = "";
|
private static final String DEFAULT_VALUE = "";
|
||||||
|
@ -70,7 +70,7 @@ public class PasswordEncryptedPreference extends DialogPreference
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
key = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, KEY_ALIAS);
|
key = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, Constants.KEYSTORE_ALIAS_PASSWORD);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {}
|
|
||||||
}
|
|
|
@ -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";
|
||||||
|
}
|
|
@ -25,35 +25,90 @@ package org.shadowice.flocke.andotp.Utilities;
|
||||||
|
|
||||||
import android.app.backup.BackupManager;
|
import android.app.backup.BackupManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.shadowice.flocke.andotp.Database.Entry;
|
import org.shadowice.flocke.andotp.Database.Entry;
|
||||||
|
import org.shadowice.flocke.andotp.R;
|
||||||
|
|
||||||
import java.io.File;
|
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 java.util.ArrayList;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
public class DatabaseHelper {
|
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();
|
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<Entry> 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<Entry> entries) {
|
|
||||||
String jsonString = entriesToString(entries);
|
String jsonString = entriesToString(entries);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
synchronized (DatabaseHelper.DatabaseFileLock) {
|
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));
|
FileHelper.writeBytesToFile(new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE), data);
|
||||||
data = EncryptionHelper.encrypt(key, data);
|
|
||||||
|
|
||||||
FileHelper.writeBytesToFile(new File(context.getFilesDir() + "/" + SETTINGS_FILE), data);
|
|
||||||
}
|
}
|
||||||
} catch (Exception error) {
|
} catch (Exception error) {
|
||||||
error.printStackTrace();
|
error.printStackTrace();
|
||||||
|
@ -66,21 +121,23 @@ public class DatabaseHelper {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ArrayList<Entry> loadDatabase(Context context){
|
public static ArrayList<Entry> loadDatabase(Context context, SecretKey encryptionKey) {
|
||||||
ArrayList<Entry> entries = new ArrayList<>();
|
ArrayList<Entry> entries = new ArrayList<>();
|
||||||
|
|
||||||
|
if (encryptionKey != null) {
|
||||||
try {
|
try {
|
||||||
synchronized (DatabaseHelper.DatabaseFileLock) {
|
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) {
|
} catch (Exception error) {
|
||||||
error.printStackTrace();
|
error.printStackTrace();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, R.string.toast_encryption_key_empty, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
@ -117,12 +174,4 @@ public class DatabaseHelper {
|
||||||
|
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Export functions */
|
|
||||||
|
|
||||||
public static boolean exportAsJSON(Context context, Uri file) {
|
|
||||||
ArrayList<Entry> entries = loadDatabase(context);
|
|
||||||
|
|
||||||
return FileHelper.writeStringToFile(context, file, entriesToString(entries));
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -23,30 +23,71 @@
|
||||||
|
|
||||||
package org.shadowice.flocke.andotp.Utilities;
|
package org.shadowice.flocke.andotp.Utilities;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.InvalidAlgorithmParameterException;
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.KeyPair;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.security.spec.KeySpec;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException;
|
import javax.crypto.BadPaddingException;
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
import javax.crypto.IllegalBlockSizeException;
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
import javax.crypto.NoSuchPaddingException;
|
import javax.crypto.NoSuchPaddingException;
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.SecretKeyFactory;
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.PBEKeySpec;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
public class EncryptionHelper {
|
public class EncryptionHelper {
|
||||||
private final static String ALGORITHM_SYMMETRIC = "AES/GCM/NoPadding";
|
public static class PBKDF2Credentials {
|
||||||
private final static String ALGORITHM_ASYMMETRIC = "RSA/ECB/PKCS1Padding";
|
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)
|
public static SecretKey generateSymmetricKeyFromPassword(String password)
|
||||||
throws NoSuchAlgorithmException {
|
throws NoSuchAlgorithmException {
|
||||||
|
@ -57,7 +98,7 @@ public class EncryptionHelper {
|
||||||
|
|
||||||
public static byte[] encrypt(SecretKey secretKey, IvParameterSpec iv, byte[] plainText)
|
public static byte[] encrypt(SecretKey secretKey, IvParameterSpec iv, byte[] plainText)
|
||||||
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException {
|
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);
|
cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
|
||||||
|
|
||||||
return cipher.doFinal(plainText);
|
return cipher.doFinal(plainText);
|
||||||
|
@ -65,7 +106,7 @@ public class EncryptionHelper {
|
||||||
|
|
||||||
public static byte[] encrypt(SecretKey secretKey, byte[] plaintext)
|
public static byte[] encrypt(SecretKey secretKey, byte[] plaintext)
|
||||||
throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, UnsupportedEncodingException, InvalidAlgorithmParameterException {
|
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);
|
new SecureRandom().nextBytes(iv);
|
||||||
|
|
||||||
byte[] cipherText = encrypt(secretKey, new IvParameterSpec(iv), plaintext);
|
byte[] cipherText = encrypt(secretKey, new IvParameterSpec(iv), plaintext);
|
||||||
|
@ -79,7 +120,7 @@ public class EncryptionHelper {
|
||||||
|
|
||||||
public static byte[] encrypt(PublicKey publicKey, byte[] plaintext)
|
public static byte[] encrypt(PublicKey publicKey, byte[] plaintext)
|
||||||
throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, UnsupportedEncodingException, InvalidAlgorithmParameterException {
|
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);
|
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
||||||
|
|
||||||
return cipher.doFinal(plaintext);
|
return cipher.doFinal(plaintext);
|
||||||
|
@ -87,7 +128,7 @@ public class EncryptionHelper {
|
||||||
|
|
||||||
public static byte[] decrypt(SecretKey secretKey, IvParameterSpec iv, byte[] cipherText)
|
public static byte[] decrypt(SecretKey secretKey, IvParameterSpec iv, byte[] cipherText)
|
||||||
throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
|
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);
|
cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
|
||||||
|
|
||||||
return cipher.doFinal(cipherText);
|
return cipher.doFinal(cipherText);
|
||||||
|
@ -95,17 +136,44 @@ public class EncryptionHelper {
|
||||||
|
|
||||||
public static byte[] decrypt(SecretKey secretKey, byte[] cipherText)
|
public static byte[] decrypt(SecretKey secretKey, byte[] cipherText)
|
||||||
throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
|
throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
|
||||||
byte[] iv = Arrays.copyOfRange(cipherText, 0, IV_LENGTH);
|
byte[] iv = Arrays.copyOfRange(cipherText, 0, Constants.ENCRYPTION_IV_LENGTH);
|
||||||
byte[] encrypted = Arrays.copyOfRange(cipherText, IV_LENGTH, cipherText.length);
|
byte[] encrypted = Arrays.copyOfRange(cipherText, Constants.ENCRYPTION_IV_LENGTH, cipherText.length);
|
||||||
|
|
||||||
return decrypt(secretKey, new IvParameterSpec(iv), encrypted);
|
return decrypt(secretKey, new IvParameterSpec(iv), encrypted);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] decrypt(PrivateKey privateKey, byte[] cipherText)
|
public static byte[] decrypt(PrivateKey privateKey, byte[] cipherText)
|
||||||
throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
|
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);
|
cipher.init(Cipher.DECRYPT_MODE, privateKey);
|
||||||
|
|
||||||
return cipher.doFinal(cipherText);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,8 @@ import android.security.KeyPairGeneratorSpec;
|
||||||
import android.security.keystore.KeyGenParameterSpec;
|
import android.security.keystore.KeyGenParameterSpec;
|
||||||
import android.security.keystore.KeyProperties;
|
import android.security.keystore.KeyProperties;
|
||||||
|
|
||||||
|
import org.shadowice.flocke.andotp.R;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
@ -35,17 +37,29 @@ import java.security.GeneralSecurityException;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.KeyPairGenerator;
|
import java.security.KeyPairGenerator;
|
||||||
import java.security.KeyStore;
|
import java.security.KeyStore;
|
||||||
import java.security.SecureRandom;
|
import java.security.ProviderException;
|
||||||
import java.security.spec.AlgorithmParameterSpec;
|
import java.security.spec.AlgorithmParameterSpec;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.GregorianCalendar;
|
import java.util.GregorianCalendar;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
import javax.security.auth.x500.X500Principal;
|
import javax.security.auth.x500.X500Principal;
|
||||||
|
|
||||||
public class KeyStoreHelper {
|
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)
|
public static KeyPair loadOrGenerateAsymmetricKeyPair(Context context, String alias)
|
||||||
throws GeneralSecurityException, IOException {
|
throws GeneralSecurityException, IOException {
|
||||||
|
@ -83,34 +97,26 @@ public class KeyStoreHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
final KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, null);
|
final KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, null);
|
||||||
|
|
||||||
|
if (entry != null)
|
||||||
return new KeyPair(entry.getCertificate().getPublicKey(), entry.getPrivateKey());
|
return new KeyPair(entry.getCertificate().getPublicKey(), entry.getPrivateKey());
|
||||||
|
else
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static SecretKey loadEncryptionKeyFromKeyStore(Context context, boolean failSilent) {
|
||||||
* Load our symmetric secret key.
|
SecretKey encKey = null;
|
||||||
* 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");
|
|
||||||
|
|
||||||
// Generate secret key if none exists
|
try {
|
||||||
if (!keyFile.exists()) {
|
KeyPair pair = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, Constants.KEYSTORE_ALIAS_WRAPPING);
|
||||||
final byte[] raw = new byte[KEY_LENGTH];
|
if (pair != null)
|
||||||
new SecureRandom().nextBytes(raw);
|
encKey = EncryptionHelper.loadOrGenerateWrappedKey(new File(context.getFilesDir() + "/" + Constants.FILENAME_ENCRYPTED_KEY), pair);
|
||||||
|
} catch (GeneralSecurityException | IOException | ProviderException e) {
|
||||||
final SecretKey key = new SecretKeySpec(raw, "AES");
|
e.printStackTrace();
|
||||||
final byte[] wrapped = wrapper.wrap(key);
|
if (! failSilent)
|
||||||
|
UIHelper.showGenericDialog(context, R.string.dialog_title_keystore_error, R.string.dialog_msg_keystore_error);
|
||||||
|
|
||||||
FileHelper.writeBytesToFile(keyFile, wrapped);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Even if we just generated the key, always read it back to ensure we
|
return encKey;
|
||||||
// can read it successfully.
|
|
||||||
final byte[] wrapped = FileHelper.readFileToBytes(keyFile);
|
|
||||||
|
|
||||||
return wrapper.unwrap(wrapped);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
package org.shadowice.flocke.andotp.Utilities;
|
package org.shadowice.flocke.andotp.Utilities;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
|
@ -46,10 +45,10 @@ public class SecretKeyWrapper {
|
||||||
* If no pair with that alias exists, it will be generated.
|
* If no pair with that alias exists, it will be generated.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("GetInstance")
|
@SuppressLint("GetInstance")
|
||||||
public SecretKeyWrapper(Context context, String alias)
|
public SecretKeyWrapper(KeyPair keyPair)
|
||||||
throws GeneralSecurityException, IOException {
|
throws GeneralSecurityException, IOException {
|
||||||
mCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
|
mCipher = Cipher.getInstance(Constants.ALGORITHM_ASYMMETRIC);
|
||||||
mPair = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, alias);
|
mPair = keyPair;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -24,38 +24,29 @@ package org.shadowice.flocke.andotp.Utilities;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Environment;
|
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
|
|
||||||
import org.apache.commons.codec.binary.Hex;
|
import org.shadowice.flocke.andotp.Preferences.CredentialsPreference;
|
||||||
import org.apache.commons.codec.digest.DigestUtils;
|
|
||||||
import org.shadowice.flocke.andotp.R;
|
import org.shadowice.flocke.andotp.R;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
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 {
|
public class Settings {
|
||||||
private static final String DEFAULT_BACKUP_FOLDER = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "andOTP";
|
|
||||||
|
|
||||||
private Context context;
|
private Context context;
|
||||||
private SharedPreferences settings;
|
private SharedPreferences settings;
|
||||||
|
|
||||||
public enum AuthMethod {
|
|
||||||
NONE, PASSWORD, PIN, DEVICE
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum SortMode {
|
|
||||||
UNSORTED, LABEL, LAST_USED
|
|
||||||
}
|
|
||||||
|
|
||||||
public Settings(Context context) {
|
public Settings(Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.settings = PreferenceManager.getDefaultSharedPreferences(context);
|
this.settings = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
@ -67,26 +58,18 @@ public class Settings {
|
||||||
private void setupDeviceDependedDefaults() {
|
private void setupDeviceDependedDefaults() {
|
||||||
if (! settings.contains(getResString(R.string.settings_key_backup_directory))
|
if (! settings.contains(getResString(R.string.settings_key_backup_directory))
|
||||||
|| settings.getString(getResString(R.string.settings_key_backup_directory), "").isEmpty()) {
|
|| 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() {
|
private void migrateDeprecatedSettings() {
|
||||||
if (settings.contains(getResString(R.string.settings_key_auth_password))) {
|
if (settings.contains(getResString(R.string.settings_key_auth_password))) {
|
||||||
String plainPassword = getAuthPassword();
|
setAuthCredentials(getString(R.string.settings_key_auth_password, ""));
|
||||||
String hashedPassword = new String(Hex.encodeHex(DigestUtils.sha256(plainPassword)));
|
|
||||||
|
|
||||||
setString(R.string.settings_key_auth_password_hash, hashedPassword);
|
|
||||||
|
|
||||||
remove(R.string.settings_key_auth_password);
|
remove(R.string.settings_key_auth_password);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.contains(getResString(R.string.settings_key_auth_pin))) {
|
if (settings.contains(getResString(R.string.settings_key_auth_pin))) {
|
||||||
String plainPIN = getAuthPIN();
|
setAuthCredentials(getString(R.string.settings_key_auth_pin, ""));
|
||||||
String hashedPIN = new String(Hex.encodeHex(DigestUtils.sha256(plainPIN)));
|
|
||||||
|
|
||||||
setString(R.string.settings_key_auth_pin_hash, hashedPIN);
|
|
||||||
|
|
||||||
remove(R.string.settings_key_auth_pin);
|
remove(R.string.settings_key_auth_pin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +77,7 @@ public class Settings {
|
||||||
String plainPassword = getBackupPassword();
|
String plainPassword = getBackupPassword();
|
||||||
|
|
||||||
try {
|
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));
|
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));
|
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));
|
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) {
|
private long getLong(int keyId, long defaultValue) {
|
||||||
return settings.getLong(getResString(keyId), defaultValue);
|
return settings.getLong(getResString(keyId), defaultValue);
|
||||||
}
|
}
|
||||||
|
@ -144,6 +131,12 @@ public class Settings {
|
||||||
.apply();
|
.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setInt(int keyId, int value) {
|
||||||
|
settings.edit()
|
||||||
|
.putInt(getResString(keyId), value)
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
|
||||||
private void setString(int keyId, String value) {
|
private void setString(int keyId, String value) {
|
||||||
settings.edit()
|
settings.edit()
|
||||||
.putString(getResString(keyId), value)
|
.putString(getResString(keyId), value)
|
||||||
|
@ -163,9 +156,10 @@ public class Settings {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clear(boolean keep_auth) {
|
public void clear(boolean keep_auth) {
|
||||||
String authMethod = getAuthMethod().toString().toLowerCase();
|
AuthMethod authMethod = getAuthMethod();
|
||||||
String authPassword = getAuthPasswordHash();
|
String authCredentials = getAuthCredentials();
|
||||||
String authPIN = getAuthPINHash();
|
byte[] authSalt = getSalt();
|
||||||
|
int authIterations = getIterations();
|
||||||
|
|
||||||
boolean warningShown = getFirstTimeWarningShown();
|
boolean warningShown = getFirstTimeWarningShown();
|
||||||
|
|
||||||
|
@ -175,13 +169,15 @@ public class Settings {
|
||||||
editor.putBoolean(getResString(R.string.settings_key_security_backup_warning), warningShown);
|
editor.putBoolean(getResString(R.string.settings_key_security_backup_warning), warningShown);
|
||||||
|
|
||||||
if (keep_auth) {
|
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())
|
if (! authCredentials.isEmpty()) {
|
||||||
editor.putString(getResString(R.string.settings_key_auth_password_hash), authPassword);
|
editor.putString(getResString(R.string.settings_key_auth_credentials), authCredentials);
|
||||||
|
editor.putInt(getResString(R.string.settings_key_auth_iterations), authIterations);
|
||||||
|
|
||||||
if (!authPIN.isEmpty())
|
String encodedSalt = Base64.encodeToString(authSalt, Base64.URL_SAFE);
|
||||||
editor.putString(getResString(R.string.settings_key_auth_pin_hash), authPIN);
|
editor.putString(getResString(R.string.settings_key_auth_salt), encodedSalt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.commit();
|
editor.commit();
|
||||||
|
@ -206,24 +202,82 @@ public class Settings {
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthMethod getAuthMethod() {
|
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());
|
return AuthMethod.valueOf(authString.toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAuthPassword() {
|
public void removeAuthPasswordHash() {
|
||||||
return getString(R.string.settings_key_auth_password, "");
|
remove(R.string.settings_key_auth_password_hash);
|
||||||
|
}
|
||||||
|
public void removeAuthPINHash() {
|
||||||
|
remove(R.string.settings_key_auth_pin_hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAuthPasswordHash() {
|
public String getOldCredentials(AuthMethod method) {
|
||||||
|
if (method == AuthMethod.PASSWORD)
|
||||||
return getString(R.string.settings_key_auth_password_hash, "");
|
return getString(R.string.settings_key_auth_password_hash, "");
|
||||||
}
|
else if (method == AuthMethod.PIN)
|
||||||
|
|
||||||
public String getAuthPIN() {
|
|
||||||
return getString(R.string.settings_key_auth_pin, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAuthPINHash() {
|
|
||||||
return getString(R.string.settings_key_auth_pin_hash, "");
|
return getString(R.string.settings_key_auth_pin_hash, "");
|
||||||
|
else
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthCredentials() {
|
||||||
|
return getString(R.string.settings_key_auth_credentials, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String> getPanicResponse() {
|
public Set<String> getPanicResponse() {
|
||||||
|
@ -281,7 +335,7 @@ public class Settings {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getBackupDir() {
|
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() {
|
public String getBackupPassword() {
|
||||||
|
@ -295,7 +349,7 @@ public class Settings {
|
||||||
String password = "";
|
String password = "";
|
||||||
|
|
||||||
try {
|
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);
|
password = new String(EncryptionHelper.decrypt(key.getPrivate(), encPassword), StandardCharsets.UTF_8);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -62,7 +62,9 @@ import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.Callable;
|
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<EntryViewHolder>
|
public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
||||||
implements ItemTouchHelperAdapter, Filterable {
|
implements ItemTouchHelperAdapter, Filterable {
|
||||||
|
@ -74,6 +76,8 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
||||||
private Callback callback;
|
private Callback callback;
|
||||||
private List<String> tagsFilter = new ArrayList<>();
|
private List<String> tagsFilter = new ArrayList<>();
|
||||||
|
|
||||||
|
private SecretKey encryptionKey = null;
|
||||||
|
|
||||||
private SortMode sortMode = SortMode.UNSORTED;
|
private SortMode sortMode = SortMode.UNSORTED;
|
||||||
private TagsAdapter tagsFilterAdapter;
|
private TagsAdapter tagsFilterAdapter;
|
||||||
private Settings settings;
|
private Settings settings;
|
||||||
|
@ -83,8 +87,15 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
||||||
this.tagsFilterAdapter = tagsFilterAdapter;
|
this.tagsFilterAdapter = tagsFilterAdapter;
|
||||||
this.settings = new Settings(context);
|
this.settings = new Settings(context);
|
||||||
this.taskHandler = new Handler();
|
this.taskHandler = new Handler();
|
||||||
|
this.entries = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
loadEntries();
|
public void setEncryptionKey(SecretKey key) {
|
||||||
|
encryptionKey = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SecretKey getEncryptionKey() {
|
||||||
|
return encryptionKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -92,6 +103,10 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
||||||
return displayedEntries.size();
|
return displayedEntries.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ArrayList<Entry> getEntries() {
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
public void addEntry(Entry e) {
|
public void addEntry(Entry e) {
|
||||||
if (! entries.contains(e)) {
|
if (! entries.contains(e)) {
|
||||||
entries.add(e);
|
entries.add(e);
|
||||||
|
@ -112,13 +127,15 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveEntries() {
|
public void saveEntries() {
|
||||||
DatabaseHelper.saveDatabase(context, entries);
|
DatabaseHelper.saveDatabase(context, entries, encryptionKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void loadEntries() {
|
public void loadEntries() {
|
||||||
entries = DatabaseHelper.loadDatabase(context);
|
if (encryptionKey != null) {
|
||||||
|
entries = DatabaseHelper.loadDatabase(context, encryptionKey);
|
||||||
entriesChanged();
|
entriesChanged();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void filterByTags(List<String> tags) {
|
public void filterByTags(List<String> tags) {
|
||||||
tagsFilter = tags;
|
tagsFilter = tags;
|
||||||
|
@ -255,7 +272,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
||||||
displayedEntries.get(position).setLastUsed(timeStamp);
|
displayedEntries.get(position).setLastUsed(timeStamp);
|
||||||
|
|
||||||
entries.get(realIndex).setLastUsed(timeStamp);
|
entries.get(realIndex).setLastUsed(timeStamp);
|
||||||
DatabaseHelper.saveDatabase(context, entries);
|
DatabaseHelper.saveDatabase(context, entries, encryptionKey);
|
||||||
|
|
||||||
if (sortMode == SortMode.LAST_USED) {
|
if (sortMode == SortMode.LAST_USED) {
|
||||||
displayedEntries = sortEntries(displayedEntries);
|
displayedEntries = sortEntries(displayedEntries);
|
||||||
|
@ -274,7 +291,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
||||||
displayedEntries = new ArrayList<>(entries);
|
displayedEntries = new ArrayList<>(entries);
|
||||||
notifyItemMoved(fromPosition, toPosition);
|
notifyItemMoved(fromPosition, toPosition);
|
||||||
|
|
||||||
DatabaseHelper.saveDatabase(context, entries);
|
DatabaseHelper.saveDatabase(context, entries, encryptionKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -314,7 +331,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
||||||
Entry e = entries.get(realIndex);
|
Entry e = entries.get(realIndex);
|
||||||
e.setLabel(newLabel);
|
e.setLabel(newLabel);
|
||||||
|
|
||||||
DatabaseHelper.saveDatabase(context, entries);
|
DatabaseHelper.saveDatabase(context, entries, encryptionKey);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
|
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
|
||||||
|
@ -395,7 +412,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
||||||
Entry e = entries.get(realIndex);
|
Entry e = entries.get(realIndex);
|
||||||
e.setThumbnail(thumbnail);
|
e.setThumbnail(thumbnail);
|
||||||
|
|
||||||
DatabaseHelper.saveDatabase(context, entries);
|
DatabaseHelper.saveDatabase(context, entries, encryptionKey);
|
||||||
notifyItemChanged(pos);
|
notifyItemChanged(pos);
|
||||||
alert.cancel();
|
alert.cancel();
|
||||||
}
|
}
|
||||||
|
@ -423,7 +440,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
||||||
@Override
|
@Override
|
||||||
public Object call() throws Exception {
|
public Object call() throws Exception {
|
||||||
entries.get(realPos).setTags(tagsAdapter.getActiveTags());
|
entries.get(realPos).setTags(tagsAdapter.getActiveTags());
|
||||||
DatabaseHelper.saveDatabase(context, entries);
|
DatabaseHelper.saveDatabase(context, entries, encryptionKey);
|
||||||
|
|
||||||
List<String> inUseTags = getTags();
|
List<String> inUseTags = getTags();
|
||||||
|
|
||||||
|
@ -468,7 +485,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
||||||
notifyItemRemoved(pos);
|
notifyItemRemoved(pos);
|
||||||
|
|
||||||
entries.remove(realIndex);
|
entries.remove(realIndex);
|
||||||
DatabaseHelper.saveDatabase(context, entries);
|
DatabaseHelper.saveDatabase(context, entries, encryptionKey);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
|
.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
|
||||||
|
|
|
@ -43,7 +43,6 @@ import java.util.List;
|
||||||
|
|
||||||
public class EntryViewHolder extends RecyclerView.ViewHolder
|
public class EntryViewHolder extends RecyclerView.ViewHolder
|
||||||
implements ItemTouchHelperViewHolder {
|
implements ItemTouchHelperViewHolder {
|
||||||
|
|
||||||
private Context context;
|
private Context context;
|
||||||
private Callback callback;
|
private Callback callback;
|
||||||
private boolean tapToReveal;
|
private boolean tapToReveal;
|
||||||
|
|
83
app/src/main/res/layout/component_authentication.xml
Normal file
83
app/src/main/res/layout/component_authentication.xml
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="@dimen/activity_margin_medium"
|
||||||
|
android:paddingBottom="@dimen/activity_margin_small"
|
||||||
|
android:paddingStart="@dimen/activity_margin_medium"
|
||||||
|
android:paddingEnd="@dimen/activity_margin_medium" >
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
android:id="@+id/credentialSelection"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:choiceMode="singleChoice" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/credentialsLayout"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="@dimen/activity_margin"
|
||||||
|
android:paddingEnd="@dimen/activity_margin">
|
||||||
|
|
||||||
|
<android.support.design.widget.TextInputLayout
|
||||||
|
android:id="@+id/passwordLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/settings_hint_password"
|
||||||
|
app:passwordToggleEnabled="true" >
|
||||||
|
|
||||||
|
<android.support.design.widget.TextInputEditText
|
||||||
|
android:id="@+id/passwordEdit"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:inputType="textPassword" />
|
||||||
|
|
||||||
|
</android.support.design.widget.TextInputLayout>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/passwordConfirm"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/settings_hint_password_confirm"
|
||||||
|
android:inputType="textPassword" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/toShortWarning"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingBottom="@dimen/activity_margin"
|
||||||
|
android:paddingTop="@dimen/activity_margin"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:text="@string/settings_label_short_password" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<android.support.v7.widget.ButtonBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/activity_margin"
|
||||||
|
android:gravity="end" >
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnCancel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
style="?android:attr/buttonBarButtonStyle"
|
||||||
|
android:text="@string/button_cancel" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnSave"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
style="?android:attr/buttonBarButtonStyle"
|
||||||
|
android:text="@string/button_save" />
|
||||||
|
|
||||||
|
</android.support.v7.widget.ButtonBarLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -15,7 +15,7 @@
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:text="@string/auth_msg_password"/>
|
android:text="@string/auth_msg_authenticate"/>
|
||||||
|
|
||||||
<android.support.design.widget.TextInputLayout
|
<android.support.design.widget.TextInputLayout
|
||||||
android:id="@+id/passwordLayout"
|
android:id="@+id/passwordLayout"
|
||||||
|
@ -33,4 +33,12 @@
|
||||||
|
|
||||||
</android.support.design.widget.TextInputLayout>
|
</android.support.design.widget.TextInputLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/buttonUnlock"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
style="?android:attr/buttonBarButtonStyle"
|
||||||
|
android:text="@string/auth_button_unlock" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
60
app/src/main/res/layout/dialog_database_encryption.xml
Normal file
60
app/src/main/res/layout/dialog_database_encryption.xml
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingTop="@dimen/activity_margin"
|
||||||
|
android:paddingBottom="@dimen/activity_margin_small">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginStart="@dimen/activity_margin_medium"
|
||||||
|
android:layout_marginEnd="@dimen/activity_margin_medium">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:text="@string/dialog_msg_security_first" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/activity_margin_small"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:text="@string/dialog_title_security_keystore"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/activity_margin_small"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:text="@string/dialog_msg_security_keystore" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/activity_margin_small"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:text="@string/dialog_title_security_password"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/activity_margin_small"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:text="@string/dialog_msg_security_password" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/activity_margin_small"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:text="@string/dialog_msg_security_default" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
|
@ -1,39 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:paddingTop="@dimen/activity_margin"
|
|
||||||
android:paddingBottom="@dimen/activity_margin_small"
|
|
||||||
android:paddingStart="@dimen/activity_margin_medium"
|
|
||||||
android:paddingEnd="@dimen/activity_margin_medium">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
|
||||||
android:text="@string/dialog_msg_security_backup_desc" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/activity_margin_small"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
|
||||||
android:text="@string/dialog_msg_security_backup_3rd_party" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/activity_margin_small"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:text="@string/dialog_msg_security_backup_warning" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/activity_margin_small"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
|
||||||
android:text="@string/dialog_msg_security_backup_one_time" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
|
@ -41,7 +41,7 @@
|
||||||
<string name="menu_popup_edit_tags">Upravit štítky</string>
|
<string name="menu_popup_edit_tags">Upravit štítky</string>
|
||||||
<string name="menu_popup_remove">Smazat</string>
|
<string name="menu_popup_remove">Smazat</string>
|
||||||
<!-- Toast messages -->
|
<!-- Toast messages -->
|
||||||
<string name="toast_auth_failed">Ověření se nezdařilo, zavírám andOTP!</string>
|
<string name="toast_auth_failed_fatal">Ověření se nezdařilo, zavírám andOTP!</string>
|
||||||
<string name="toast_copied_to_clipboard">Zkopírováno do schránky</string>
|
<string name="toast_copied_to_clipboard">Zkopírováno do schránky</string>
|
||||||
<string name="toast_entry_exists">Tento záznam již existuje</string>
|
<string name="toast_entry_exists">Tento záznam již existuje</string>
|
||||||
<string name="toast_invalid_qr_code">Neplatný QR kód</string>
|
<string name="toast_invalid_qr_code">Neplatný QR kód</string>
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
<string name="menu_popup_edit_tags">Marker bearbeiten</string>
|
<string name="menu_popup_edit_tags">Marker bearbeiten</string>
|
||||||
<string name="menu_popup_remove">Entfernen</string>
|
<string name="menu_popup_remove">Entfernen</string>
|
||||||
<!-- Toast messages -->
|
<!-- Toast messages -->
|
||||||
<string name="toast_auth_failed">Authentifizierung fehlgeschlagen, andOTP wird geschlossen!</string>
|
<string name="toast_auth_failed_fatal">Authentifizierung fehlgeschlagen, andOTP wird geschlossen!</string>
|
||||||
<string name="toast_copied_to_clipboard">In Zwischenablage kopiert</string>
|
<string name="toast_copied_to_clipboard">In Zwischenablage kopiert</string>
|
||||||
<string name="toast_entry_exists">Dieser Eintrag ist bereits vorhanden</string>
|
<string name="toast_entry_exists">Dieser Eintrag ist bereits vorhanden</string>
|
||||||
<string name="toast_invalid_qr_code">Ungültiger QR-Code</string>
|
<string name="toast_invalid_qr_code">Ungültiger QR-Code</string>
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
<string name="menu_popup_edit_tags">Editar etiquetas</string>
|
<string name="menu_popup_edit_tags">Editar etiquetas</string>
|
||||||
<string name="menu_popup_remove">Eliminar</string>
|
<string name="menu_popup_remove">Eliminar</string>
|
||||||
<!-- Toast messages -->
|
<!-- Toast messages -->
|
||||||
<string name="toast_auth_failed">La autenticación ha fallado, cerrando andOTP!</string>
|
<string name="toast_auth_failed_fatal">La autenticación ha fallado, cerrando andOTP!</string>
|
||||||
<string name="toast_copied_to_clipboard">Copiado al portapapeles</string>
|
<string name="toast_copied_to_clipboard">Copiado al portapapeles</string>
|
||||||
<string name="toast_entry_exists">La entrada ya existe</string>
|
<string name="toast_entry_exists">La entrada ya existe</string>
|
||||||
<string name="toast_invalid_qr_code">Código QR inválido</string>
|
<string name="toast_invalid_qr_code">Código QR inválido</string>
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
<string name="menu_popup_edit_tags">Éditer les tags</string>
|
<string name="menu_popup_edit_tags">Éditer les tags</string>
|
||||||
<string name="menu_popup_remove">Supprimer</string>
|
<string name="menu_popup_remove">Supprimer</string>
|
||||||
<!-- Toast messages -->
|
<!-- Toast messages -->
|
||||||
<string name="toast_auth_failed">
|
<string name="toast_auth_failed_fatal">
|
||||||
L\'authentification a échoué, fermeture d’andOTP !
|
L\'authentification a échoué, fermeture d’andOTP !
|
||||||
</string>
|
</string>
|
||||||
<string name="toast_copied_to_clipboard">
|
<string name="toast_copied_to_clipboard">
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
<string name="menu_popup_edit_tags">Editar etiquetas</string>
|
<string name="menu_popup_edit_tags">Editar etiquetas</string>
|
||||||
<string name="menu_popup_remove">Eliminar</string>
|
<string name="menu_popup_remove">Eliminar</string>
|
||||||
<!-- Toast messages -->
|
<!-- Toast messages -->
|
||||||
<string name="toast_auth_failed">A autenticación fallou, cerrando andOTP!</string>
|
<string name="toast_auth_failed_fatal">A autenticación fallou, cerrando andOTP!</string>
|
||||||
<string name="toast_copied_to_clipboard">Copiado ao portapapeis</string>
|
<string name="toast_copied_to_clipboard">Copiado ao portapapeis</string>
|
||||||
<string name="toast_entry_exists">Este nome xa existe</string>
|
<string name="toast_entry_exists">Este nome xa existe</string>
|
||||||
<string name="toast_invalid_qr_code">Código QR inválido</string>
|
<string name="toast_invalid_qr_code">Código QR inválido</string>
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
<string name="menu_popup_edit_tags">Labels bewerken</string>
|
<string name="menu_popup_edit_tags">Labels bewerken</string>
|
||||||
<string name="menu_popup_remove">Verwijderen</string>
|
<string name="menu_popup_remove">Verwijderen</string>
|
||||||
<!-- Toast messages -->
|
<!-- Toast messages -->
|
||||||
<string name="toast_auth_failed">Verificatie is mislukt, sluiten andOTP!</string>
|
<string name="toast_auth_failed_fatal">Verificatie is mislukt, sluiten andOTP!</string>
|
||||||
<string name="toast_copied_to_clipboard">Gekopieerd naar klembord</string>
|
<string name="toast_copied_to_clipboard">Gekopieerd naar klembord</string>
|
||||||
<string name="toast_entry_exists">Deze item bestaat al</string>
|
<string name="toast_entry_exists">Deze item bestaat al</string>
|
||||||
<string name="toast_invalid_qr_code">Ongeldige QR Code</string>
|
<string name="toast_invalid_qr_code">Ongeldige QR Code</string>
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
<string name="menu_popup_edit_tags">Edytuj znaczniki</string>
|
<string name="menu_popup_edit_tags">Edytuj znaczniki</string>
|
||||||
<string name="menu_popup_remove">Usuń</string>
|
<string name="menu_popup_remove">Usuń</string>
|
||||||
<!-- Toast messages -->
|
<!-- Toast messages -->
|
||||||
<string name="toast_auth_failed">Autoryzacja nie powiodła się, zamykam andOTP!</string>
|
<string name="toast_auth_failed_fatal">Autoryzacja nie powiodła się, zamykam andOTP!</string>
|
||||||
<string name="toast_copied_to_clipboard">Skopiowano do schowka</string>
|
<string name="toast_copied_to_clipboard">Skopiowano do schowka</string>
|
||||||
<string name="toast_entry_exists">Taki rekord już istnieje</string>
|
<string name="toast_entry_exists">Taki rekord już istnieje</string>
|
||||||
<string name="toast_invalid_qr_code">Nieprawidłowy kod QR</string>
|
<string name="toast_invalid_qr_code">Nieprawidłowy kod QR</string>
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
<string name="menu_popup_edit_tags">Изменить теги</string>
|
<string name="menu_popup_edit_tags">Изменить теги</string>
|
||||||
<string name="menu_popup_remove">Убрать</string>
|
<string name="menu_popup_remove">Убрать</string>
|
||||||
<!-- Toast messages -->
|
<!-- Toast messages -->
|
||||||
<string name="toast_auth_failed">Ошибка аутентификации, andOTP закрывается!</string>
|
<string name="toast_auth_failed_fatal">Ошибка аутентификации, andOTP закрывается!</string>
|
||||||
<string name="toast_copied_to_clipboard">Скопировано в буфер обмена</string>
|
<string name="toast_copied_to_clipboard">Скопировано в буфер обмена</string>
|
||||||
<string name="toast_entry_exists">Эта запись уже существует</string>
|
<string name="toast_entry_exists">Эта запись уже существует</string>
|
||||||
<string name="toast_invalid_qr_code">Недопустимый QR-код</string>
|
<string name="toast_invalid_qr_code">Недопустимый QR-код</string>
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
<string name="menu_popup_edit_tags">编辑标签</string>
|
<string name="menu_popup_edit_tags">编辑标签</string>
|
||||||
<string name="menu_popup_remove">移除</string>
|
<string name="menu_popup_remove">移除</string>
|
||||||
<!-- Toast messages -->
|
<!-- Toast messages -->
|
||||||
<string name="toast_auth_failed">身份验证失败,andOTP 正在关闭!</string>
|
<string name="toast_auth_failed_fatal">身份验证失败,andOTP 正在关闭!</string>
|
||||||
<string name="toast_copied_to_clipboard">已复制到剪贴板</string>
|
<string name="toast_copied_to_clipboard">已复制到剪贴板</string>
|
||||||
<string name="toast_entry_exists">该项已存在</string>
|
<string name="toast_entry_exists">该项已存在</string>
|
||||||
<string name="toast_invalid_qr_code">无效二维码</string>
|
<string name="toast_invalid_qr_code">无效二维码</string>
|
||||||
|
|
|
@ -6,10 +6,14 @@
|
||||||
<string name="settings_key_tap_to_reveal" translatable="false">pref_tap_to_reveal</string>
|
<string name="settings_key_tap_to_reveal" translatable="false">pref_tap_to_reveal</string>
|
||||||
<string name="settings_key_tap_to_reveal_timeout" translatable="false">pref_tap_to_reveal_timeout</string>
|
<string name="settings_key_tap_to_reveal_timeout" translatable="false">pref_tap_to_reveal_timeout</string>
|
||||||
<string name="settings_key_auth" translatable="false">pref_auth</string>
|
<string name="settings_key_auth" translatable="false">pref_auth</string>
|
||||||
<string name="settings_key_auth_password" translatable="false">pref_auth_password</string>
|
<string name="settings_key_auth_password" translatable="false">pref_auth_password</string> <!-- Deprecated -->
|
||||||
<string name="settings_key_auth_password_hash" translatable="false">pref_auth_password_hash</string>
|
<string name="settings_key_auth_password_hash" translatable="false">pref_auth_password_hash</string> <!-- Deprecated -->
|
||||||
<string name="settings_key_auth_pin" translatable="false">pref_auth_pin</string>
|
<string name="settings_key_auth_pin" translatable="false">pref_auth_pin</string> <!-- Deprecated -->
|
||||||
<string name="settings_key_auth_pin_hash" translatable="false">pref_auth_pin_hash</string>
|
<string name="settings_key_auth_pin_hash" translatable="false">pref_auth_pin_hash</string> <!-- Deprecated -->
|
||||||
|
<string name="settings_key_auth_credentials" translatable="false">pref_auth_credentials</string>
|
||||||
|
<string name="settings_key_auth_iterations" translatable="false">pref_auth_iterations</string>
|
||||||
|
<string name="settings_key_auth_salt" translatable="false">pref_auth_salt</string>
|
||||||
|
<string name="settings_key_encryption" translatable="false">pref_encryption</string>
|
||||||
<string name="settings_key_panic" translatable="false">pref_panic</string>
|
<string name="settings_key_panic" translatable="false">pref_panic</string>
|
||||||
|
|
||||||
<string name="settings_key_lang" translatable="false">pref_lang</string>
|
<string name="settings_key_lang" translatable="false">pref_lang</string>
|
||||||
|
@ -21,7 +25,7 @@
|
||||||
|
|
||||||
<string name="settings_key_backup_ask" translatable="false">pref_backup_ask</string>
|
<string name="settings_key_backup_ask" translatable="false">pref_backup_ask</string>
|
||||||
<string name="settings_key_backup_directory" translatable="false">pref_backup_directory</string>
|
<string name="settings_key_backup_directory" translatable="false">pref_backup_directory</string>
|
||||||
<string name="settings_key_backup_password" translatable="false">pref_backup_password</string>
|
<string name="settings_key_backup_password" translatable="false">pref_backup_password</string> <!-- Deprecated -->
|
||||||
<string name="settings_key_backup_password_enc" translatable="false">pref_backup_password_enc</string>
|
<string name="settings_key_backup_password_enc" translatable="false">pref_backup_password_enc</string>
|
||||||
<string name="settings_key_openpgp_provider" translatable="false">pref_openpgp_provider</string>
|
<string name="settings_key_openpgp_provider" translatable="false">pref_openpgp_provider</string>
|
||||||
<string name="settings_key_openpgp_keyid" translatable="false">pref_openpgp_keyid</string>
|
<string name="settings_key_openpgp_keyid" translatable="false">pref_openpgp_keyid</string>
|
||||||
|
@ -37,12 +41,13 @@
|
||||||
|
|
||||||
<string name="settings_key_enable_screenshot" translatable="false">pref_enable_screenshot</string>
|
<string name="settings_key_enable_screenshot" translatable="false">pref_enable_screenshot</string>
|
||||||
<string name="settings_key_enable_android_backup_service" translatable="false">pref_enable_android_backup_service</string>
|
<string name="settings_key_enable_android_backup_service" translatable="false">pref_enable_android_backup_service</string>
|
||||||
|
<string name="settings_key_clear_keystore" translatable="false">pref_clear_keystore</string>
|
||||||
|
|
||||||
<string name="settings_key_last_used_dialog_shown" translatable="false">pref_last_used_dialog_shown</string>
|
<string name="settings_key_last_used_dialog_shown" translatable="false">pref_last_used_dialog_shown</string>
|
||||||
|
|
||||||
<!-- Default values -->
|
<!-- Default values -->
|
||||||
<integer name="settings_default_tap_to_reveal_timeout">30</integer>
|
<integer name="settings_default_tap_to_reveal_timeout">30</integer>
|
||||||
<string name="settings_default_auth" translatable="false">none</string>
|
<string name="settings_default_encryption" translatable="false">keystore</string>
|
||||||
<string name="settings_default_lang" translatable="false">system</string>
|
<string name="settings_default_lang" translatable="false">system</string>
|
||||||
<string name="settings_default_theme" translatable="false">light</string>
|
<string name="settings_default_theme" translatable="false">light</string>
|
||||||
<integer name="settings_default_label_size">18</integer>
|
<integer name="settings_default_label_size">18</integer>
|
||||||
|
@ -61,11 +66,9 @@
|
||||||
</array>
|
</array>
|
||||||
|
|
||||||
<!-- List values -->
|
<!-- List values -->
|
||||||
<string-array name="settings_values_auth" translatable="false">
|
<string-array name="settings_values_encryption" translatable="false">
|
||||||
<item>none</item>
|
<item>keystore</item>
|
||||||
<item>password</item>
|
<item>password</item>
|
||||||
<item>pin</item>
|
|
||||||
<item>device</item>
|
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="settings_values_panic" translatable="false">
|
<string-array name="settings_values_panic" translatable="false">
|
||||||
|
|
|
@ -7,10 +7,16 @@
|
||||||
<string name="auth_hint_pin">PIN</string>
|
<string name="auth_hint_pin">PIN</string>
|
||||||
|
|
||||||
<!-- Messages -->
|
<!-- Messages -->
|
||||||
<string name="auth_msg_password">Please enter your password to start andOTP.</string>
|
<string name="auth_msg_authenticate">Please authenticate to start andOTP!</string>
|
||||||
<string name="auth_msg_pin">Please enter your PIN to start andOTP.</string>
|
<string name="auth_msg_confirm_encryption">Please confirm your authentication to generate the
|
||||||
|
new encryption key!</string>
|
||||||
|
|
||||||
|
<!-- Buttons -->
|
||||||
|
<string name="auth_button_unlock">Unlock</string>
|
||||||
|
|
||||||
<!-- Toast messages -->
|
<!-- Toast messages -->
|
||||||
<string name="auth_toast_password_missing">Please set a password in the settings!</string>
|
<string name="auth_toast_password_missing">Please set a password in the settings!</string>
|
||||||
<string name="auth_toast_pin_missing">Please set a PIN in the settings!</string>
|
<string name="auth_toast_pin_missing">Please set a PIN in the settings!</string>
|
||||||
|
<string name="auth_toast_password_again">Wrong password, please try again!</string>
|
||||||
|
<string name="auth_toast_pin_again">Wrong PIN, please try again!</string>
|
||||||
</resources>
|
</resources>
|
|
@ -7,7 +7,7 @@
|
||||||
<string name="button_scan_qr">Scan QR-Code</string>
|
<string name="button_scan_qr">Scan QR-Code</string>
|
||||||
<string name="button_save">Save</string>
|
<string name="button_save">Save</string>
|
||||||
<string name="button_new_tag">New tag</string>
|
<string name="button_new_tag">New tag</string>
|
||||||
<string name="button_warned">You have been warned!</string>
|
<string name="button_settings">Settings</string>
|
||||||
<string name="button_all_tags">All tags</string>
|
<string name="button_all_tags">All tags</string>
|
||||||
<string name="button_no_tags">No tags</string>
|
<string name="button_no_tags">No tags</string>
|
||||||
|
|
||||||
|
@ -51,36 +51,49 @@
|
||||||
<string name="menu_popup_remove">Remove</string>
|
<string name="menu_popup_remove">Remove</string>
|
||||||
|
|
||||||
<!-- Toast messages -->
|
<!-- Toast messages -->
|
||||||
<string name="toast_auth_failed">Authentication failed, closing andOTP!</string>
|
<string name="toast_auth_failed">Authentication failed, please try again!</string>
|
||||||
|
<string name="toast_auth_failed_fatal">Authentication failed, closing andOTP!</string>
|
||||||
<string name="toast_copied_to_clipboard">Copied to clipboard</string>
|
<string name="toast_copied_to_clipboard">Copied to clipboard</string>
|
||||||
<string name="toast_entry_exists">This entry already exists</string>
|
<string name="toast_entry_exists">This entry already exists</string>
|
||||||
<string name="toast_invalid_qr_code">Invalid QR Code</string>
|
<string name="toast_invalid_qr_code">Invalid QR Code</string>
|
||||||
|
<string name="toast_encryption_key_empty">Encryption key not loaded</string>
|
||||||
|
|
||||||
<!-- Dialogs -->
|
<!-- Dialogs -->
|
||||||
<string name="dialog_title_auth">Authenticate</string>
|
<string name="dialog_title_auth">Authenticate</string>
|
||||||
<string name="dialog_title_manual_entry">Enter details</string>
|
<string name="dialog_title_manual_entry">Enter details</string>
|
||||||
<string name="dialog_title_remove">Remove</string>
|
<string name="dialog_title_remove">Remove</string>
|
||||||
<string name="dialog_title_rename">Rename</string>
|
<string name="dialog_title_rename">Rename</string>
|
||||||
<string name="dialog_title_security_backup">Security and Backups</string>
|
|
||||||
<string name="dialog_title_last_used">Last used</string>
|
<string name="dialog_title_last_used">Last used</string>
|
||||||
|
<string name="dialog_title_keystore_error">KeyStore error</string>
|
||||||
|
<string name="dialog_title_encryption">Database encryption</string>
|
||||||
|
|
||||||
<string name="dialog_msg_auth">Please enter your device credentials to start andOTP.</string>
|
<string name="dialog_msg_auth">Please enter your device credentials to start andOTP.</string>
|
||||||
<string name="dialog_msg_confirm_delete">Are you sure you want do remove the account \"%1$s\"?</string>
|
<string name="dialog_msg_confirm_delete">Are you sure you want do remove the account \"%1$s\"?</string>
|
||||||
|
|
||||||
<string name="dialog_msg_security_backup_desc">To keep your account information secure this app
|
<string name="dialog_title_security_keystore">1. Android KeyStore</string>
|
||||||
only stores it encrypted. Part of the encryption key used for this is stored in the Android
|
<string name="dialog_title_security_password">2. Password / PIN</string>
|
||||||
KeyStore system. The advantage of this approach is that the key is kept separate from the
|
|
||||||
apps data and can be backed by hardware cryptography (if your device supports this).
|
<string name="dialog_msg_security_first">To ensure the security of your accounts this app
|
||||||
</string>
|
only stores them in encrypted data files using one of the following two methods:</string>
|
||||||
<string name="dialog_msg_security_backup_3rd_party">As a drawback this makes backups of the apps
|
<string name="dialog_msg_security_keystore">The KeyStore is a system component of Android for
|
||||||
data a little bit more difficult. If you use 3rd party apps (like Titanium Backup) you only
|
securely storing cryptographic keys. The advantage of this approach is that the keys are
|
||||||
backup the data files, not the encryption key and as a result such backups become useless.
|
stored separated from the data files and can be backed by hardware cryptography (if the
|
||||||
</string>
|
hardware supports it). However as the keys are not stored with the apps data this method
|
||||||
<string name="dialog_msg_security_backup_warning">Please only use the internal backup functions
|
prevents external backup solutions (like Titanium) from working. If you choose this method
|
||||||
provided by the app to backup your accounts! Anything else WILL lead to data loss.
|
you will have to rely on the internal backup functions provided by andOTP.</string>
|
||||||
</string>
|
<string name="dialog_msg_security_password">This method will encrypt your data with a key
|
||||||
<string name="dialog_msg_security_backup_one_time">This message will not be shown again.</string>
|
generated from a password or PIN. The main advantage here is that this will work with
|
||||||
|
external backup solutions (like Titanium). However you will have to enter your credentials
|
||||||
|
every time you start andOTP.</string>
|
||||||
|
<string name="dialog_msg_security_default">By default the Android KeyStore will be used, however
|
||||||
|
this is known to cause problems on certain custom ROMs (and a few stock ones as well). You
|
||||||
|
can change the encryption in the Settings by clicking on the button below.</string>
|
||||||
|
|
||||||
<string name="dialog_msg_last_used">In order for andOTP to recognize which token was used last
|
<string name="dialog_msg_last_used">In order for andOTP to recognize which token was used last
|
||||||
you have to have \"tap to reveal\" enabled or use the copy button.\n\nThis message will not
|
you have to have \"tap to reveal\" enabled or use the copy button.\n\nThis message will not
|
||||||
be shown again.</string>
|
be shown again.</string>
|
||||||
|
|
||||||
|
<string name="dialog_msg_keystore_error">Failed to load the encryption key from the KeyStore.
|
||||||
|
<b>Any entries that are added will get lost.</b>\n\nTo still be able to use andOTP you can go
|
||||||
|
to the Settings and switch the <b>Database encryption</b> to <b>Password / PIN</b>.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
<string name="settings_title_auth">Authentication</string>
|
<string name="settings_title_auth">Authentication</string>
|
||||||
<string name="settings_title_auth_password">Password</string>
|
<string name="settings_title_auth_password">Password</string>
|
||||||
<string name="settings_title_auth_pin">PIN</string>
|
<string name="settings_title_auth_pin">PIN</string>
|
||||||
|
<string name="settings_title_encryption">Database encryption</string>
|
||||||
<string name="settings_title_panic">Panic Trigger</string>
|
<string name="settings_title_panic">Panic Trigger</string>
|
||||||
|
|
||||||
<string name="settings_title_lang">Language</string>
|
<string name="settings_title_lang">Language</string>
|
||||||
|
@ -33,6 +34,7 @@
|
||||||
<string name="settings_title_special_features">Enable special features</string>
|
<string name="settings_title_special_features">Enable special features</string>
|
||||||
<string name="settings_title_enable_screenshot">Enable screenshots</string>
|
<string name="settings_title_enable_screenshot">Enable screenshots</string>
|
||||||
<string name="settings_title_enable_android_backup_service">Enable android backup</string>
|
<string name="settings_title_enable_android_backup_service">Enable android backup</string>
|
||||||
|
<string name="settings_title_clear_keystore">Clear KeyStore</string>
|
||||||
|
|
||||||
<!-- Descriptions -->
|
<!-- Descriptions -->
|
||||||
<string name="settings_desc_tap_to_reveal">Hide the OTP tokens by default, requiring them to be
|
<string name="settings_desc_tap_to_reveal">Hide the OTP tokens by default, requiring them to be
|
||||||
|
@ -55,11 +57,13 @@
|
||||||
<string name="settings_desc_openpgp_verify">Encrypted backups are only imported if they are
|
<string name="settings_desc_openpgp_verify">Encrypted backups are only imported if they are
|
||||||
signed with a valid key</string>
|
signed with a valid key</string>
|
||||||
|
|
||||||
|
<string name="settings_desc_special_features">Uncheck to disable the special features again</string>
|
||||||
<string name="settings_desc_enable_screenshot">Allow to take screenshots of the main screen
|
<string name="settings_desc_enable_screenshot">Allow to take screenshots of the main screen
|
||||||
(disabled by default for security reasons)</string>
|
(disabled by default for security reasons)</string>
|
||||||
<string name="settings_desc_enable_android_backup_service">Enables andOTP to use android\'s
|
<string name="settings_desc_enable_android_backup_service">Enables andOTP to use android\'s
|
||||||
built in backup service to bacup keys and preferences</string>
|
built in backup service to bacup keys and preferences</string>
|
||||||
<string name="settings_desc_special_features">Uncheck to disable the special features again</string>
|
<string name="settings_desc_special_features">Uncheck to disable the special features again</string>
|
||||||
|
<string name="settings_desc_clear_keystore">Delete the encryption key from the KeyStore</string>
|
||||||
|
|
||||||
<!-- Toasts -->
|
<!-- Toasts -->
|
||||||
<string name="settings_toast_auth_device_pre_lollipop">This feature requires at least Android 5.0
|
<string name="settings_toast_auth_device_pre_lollipop">This feature requires at least Android 5.0
|
||||||
|
@ -67,6 +71,44 @@
|
||||||
<string name="settings_toast_auth_device_not_secure">This feature requires a secure lock screen
|
<string name="settings_toast_auth_device_not_secure">This feature requires a secure lock screen
|
||||||
to be set up (Settings -> Security -> Screenlock)</string>
|
to be set up (Settings -> Security -> Screenlock)</string>
|
||||||
|
|
||||||
|
<string name="settings_toast_password_empty">An empty password is not allowed, set the
|
||||||
|
Authentication to \"None\" to disable it!</string>
|
||||||
|
|
||||||
|
<string name="settings_toast_encryption_changing">Trying to change the database encryption,
|
||||||
|
please wait!</string>
|
||||||
|
<string name="settings_toast_encryption_change_success">Successfully changed the database
|
||||||
|
encryption!</string>
|
||||||
|
<string name="settings_toast_encryption_change_failed">Failed to change database encryption,
|
||||||
|
restored database from internal backup!</string>
|
||||||
|
<string name="settings_toast_encryption_backup_failed">Failed to create an internal
|
||||||
|
backup, aborting!</string>
|
||||||
|
<string name="settings_toast_encryption_no_key">Failed to get the encryption key, aborting!</string>
|
||||||
|
<string name="settings_toast_encryption_auth_failed">Authentication failed, aborting!</string>
|
||||||
|
|
||||||
|
<string name="settings_toast_auth_upgrade_failed">Failed to silently upgrade your password / PIN
|
||||||
|
to the new encryption, please manually reset it in the settings!</string>
|
||||||
|
|
||||||
|
<string name="settings_dialog_title_error">Error</string>
|
||||||
|
<string name="settings_dialog_title_clear_keystore">Clear the KeyStore?</string>
|
||||||
|
|
||||||
|
<string name="settings_dialog_msg_auth_invalid_with_encryption">You can only use Password or PIN as
|
||||||
|
long as the database encryption is set to \"Password / PIN\"!</string>
|
||||||
|
<string name="settings_dialog_msg_encryption_invalid_with_auth">You first need to set the
|
||||||
|
Authentication to \"Password\" or \"PIN\"!</string>
|
||||||
|
<string name="settings_dialog_msg_encryption_invalid_without_credentials">You first need to set a
|
||||||
|
Password or PIN before changing the encryption!</string>
|
||||||
|
|
||||||
|
<string name="settings_dialog_msg_clear_keystore_password">In some cases clearing the KeyStore
|
||||||
|
can help resolve problems. You should only proceed if you know what you are doing!\n\nSince
|
||||||
|
the <b>Database encryption</b> is set to <b>Password / PIN</b> you shouldn\'t lose any data
|
||||||
|
doing this (but it never hurts to have a backup anyways).\n\n<b>Are you really sure you want
|
||||||
|
to clear the KeyStore?</b></string>
|
||||||
|
<string name="settings_dialog_msg_clear_keystore_keystore">In some cases clearing the KeyStore
|
||||||
|
can help resolve problems. You should only proceed if you know what you are doing!\n\n<b>Warning</b>:
|
||||||
|
Since the <b>Database encryption</b> is set to <b>Android KeyStore</b> you will lose all
|
||||||
|
your accounts. Make sure you have a backup!\n\n<b>Are you really sure you want to clear the
|
||||||
|
KeyStore?</b></string>
|
||||||
|
|
||||||
<!-- List entries -->
|
<!-- List entries -->
|
||||||
<string-array name="settings_entries_auth">
|
<string-array name="settings_entries_auth">
|
||||||
<item>None</item>
|
<item>None</item>
|
||||||
|
@ -75,6 +117,11 @@
|
||||||
<item>Device credentials</item>
|
<item>Device credentials</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="settings_entries_encryption">
|
||||||
|
<item>Android KeyStore</item>
|
||||||
|
<item>Password / PIN</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<string-array name="settings_entries_panic">
|
<string-array name="settings_entries_panic">
|
||||||
<item>Wipe all accounts</item>
|
<item>Wipe all accounts</item>
|
||||||
<item>Reset app settings</item>
|
<item>Reset app settings</item>
|
||||||
|
@ -97,9 +144,11 @@
|
||||||
<string name="settings_lang_sys_default">System default</string>
|
<string name="settings_lang_sys_default">System default</string>
|
||||||
|
|
||||||
<!-- PasswordPreference -->
|
<!-- PasswordPreference -->
|
||||||
<string name="settings_hint_password">Password</string>
|
<string name="settings_hint_password">Enter new password</string>
|
||||||
<string name="settings_hint_pin">PIN</string>
|
<string name="settings_hint_pin">Enter new PIN</string>
|
||||||
<string name="settings_hint_password_confirm">Confirm password</string>
|
<string name="settings_hint_password_confirm">Confirm password</string>
|
||||||
<string name="settings_hint_pin_confirm">Confirm PIN</string>
|
<string name="settings_hint_pin_confirm">Confirm PIN</string>
|
||||||
<string name="settings_hint_unchanged">(unchanged)</string>
|
|
||||||
|
<string name="settings_label_short_password">The password needs to be at least %1$d characters long!</string>
|
||||||
|
<string name="settings_label_short_pin">The PIN needs to be at least %1$d digits long!</string>
|
||||||
</resources>
|
</resources>
|
|
@ -9,14 +9,12 @@
|
||||||
|
|
||||||
<CheckBoxPreference
|
<CheckBoxPreference
|
||||||
android:key="@string/settings_key_tap_to_reveal"
|
android:key="@string/settings_key_tap_to_reveal"
|
||||||
android:order="1"
|
|
||||||
android:title="@string/settings_title_tap_to_reveal"
|
android:title="@string/settings_title_tap_to_reveal"
|
||||||
android:summary="@string/settings_desc_tap_to_reveal"
|
android:summary="@string/settings_desc_tap_to_reveal"
|
||||||
android:defaultValue="false" />
|
android:defaultValue="false" />
|
||||||
|
|
||||||
<com.vanniktech.vntnumberpickerpreference.VNTNumberPickerPreference
|
<com.vanniktech.vntnumberpickerpreference.VNTNumberPickerPreference
|
||||||
android:key="@string/settings_key_tap_to_reveal_timeout"
|
android:key="@string/settings_key_tap_to_reveal_timeout"
|
||||||
android:order="2"
|
|
||||||
android:title="@string/settings_title_tap_to_reveal_timeout"
|
android:title="@string/settings_title_tap_to_reveal_timeout"
|
||||||
android:dialogMessage="@string/settings_desc_tap_to_reveal_timeout"
|
android:dialogMessage="@string/settings_desc_tap_to_reveal_timeout"
|
||||||
android:defaultValue="@integer/settings_default_tap_to_reveal_timeout"
|
android:defaultValue="@integer/settings_default_tap_to_reveal_timeout"
|
||||||
|
@ -24,18 +22,20 @@
|
||||||
app:vnt_minValue="@integer/settings_min_tap_to_reveal_timeout"
|
app:vnt_minValue="@integer/settings_min_tap_to_reveal_timeout"
|
||||||
app:vnt_maxValue="@integer/settings_max_tap_to_reveal_timeout" />
|
app:vnt_maxValue="@integer/settings_max_tap_to_reveal_timeout" />
|
||||||
|
|
||||||
<ListPreference
|
<org.shadowice.flocke.andotp.Preferences.CredentialsPreference
|
||||||
android:key="@string/settings_key_auth"
|
android:key="@string/settings_key_auth"
|
||||||
android:order="3"
|
android:title="@string/settings_title_auth" />
|
||||||
android:title="@string/settings_title_auth"
|
|
||||||
|
<ListPreference
|
||||||
|
android:key="@string/settings_key_encryption"
|
||||||
|
android:title="@string/settings_title_encryption"
|
||||||
android:summary="%s"
|
android:summary="%s"
|
||||||
android:entries="@array/settings_entries_auth"
|
android:entries="@array/settings_entries_encryption"
|
||||||
android:entryValues="@array/settings_values_auth"
|
android:entryValues="@array/settings_values_encryption"
|
||||||
android:defaultValue="@string/settings_default_auth" />
|
android:defaultValue="@string/settings_default_encryption" />
|
||||||
|
|
||||||
<MultiSelectListPreference
|
<MultiSelectListPreference
|
||||||
android:key="@string/settings_key_panic"
|
android:key="@string/settings_key_panic"
|
||||||
android:order="5"
|
|
||||||
android:title="@string/settings_title_panic"
|
android:title="@string/settings_title_panic"
|
||||||
android:summary="@string/settings_desc_panic"
|
android:summary="@string/settings_desc_panic"
|
||||||
android:entries="@array/settings_entries_panic"
|
android:entries="@array/settings_entries_panic"
|
||||||
|
|
|
@ -23,6 +23,11 @@
|
||||||
android:summary="@string/settings_desc_enable_android_backup_service"
|
android:summary="@string/settings_desc_enable_android_backup_service"
|
||||||
android:defaultValue="false" />
|
android:defaultValue="false" />
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="@string/settings_key_clear_keystore"
|
||||||
|
android:title="@string/settings_title_clear_keystore"
|
||||||
|
android:summary="@string/settings_desc_clear_keystore" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
Loading…
Reference in a new issue