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:
RichyHBM 2018-01-11 13:58:40 +00:00
commit 41ca299af6
39 changed files with 1537 additions and 599 deletions

View file

@ -74,7 +74,7 @@ So make sure you have a **current backup** before switching!
* **Requesting thumbnails**: If you are missing a thumbnail you can request it by editing the [wiki](https://github.com/andOTP/andOTP/wiki/Thumbnails#thumbnail-requests)
* **Discussion and support**:
- [XDA thread](https://forum.xda-developers.com/android/apps-games/app-andotp-android-otp-authenticator-t3636993) (please keep off-topic to a minimum)
- Telegram channel [@andOTP](https://t.me/andOTP)
- Telegram group [@andOTP](https://t.me/andOTP) (also check out the read-only announcement channel for important updates: [@andOTP_Broadcast](https://t.me/andOTP_Broadcast))
#### Contributors:

View file

@ -32,8 +32,10 @@ import org.apache.commons.codec.binary.Hex;
import org.json.JSONException;
import org.json.JSONObject;
import org.shadowice.flocke.andotp.Database.Entry;
import org.shadowice.flocke.andotp.Utilities.Constants;
import org.shadowice.flocke.andotp.Utilities.DatabaseHelper;
import org.shadowice.flocke.andotp.Utilities.EncryptionHelper;
import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper;
import org.shadowice.flocke.andotp.Utilities.TokenCalculator;
import java.io.File;
@ -52,11 +54,10 @@ import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import static org.shadowice.flocke.andotp.Utilities.TokenCalculator.TOTP_DEFAULT_PERIOD;
public class ApplicationTest extends ApplicationTestCase<Application> {
public ApplicationTest() {
@ -69,29 +70,29 @@ public class ApplicationTest extends ApplicationTestCase<Application> {
byte[] keySHA256 = "12345678901234567890123456789012".getBytes(StandardCharsets.US_ASCII);
byte[] keySHA512 = "1234567890123456789012345678901234567890123456789012345678901234".getBytes(StandardCharsets.US_ASCII);
assertEquals(94287082, TokenCalculator.TOTP_RFC6238(keySHA1, TOTP_DEFAULT_PERIOD, 59L, 8, TokenCalculator.HashAlgorithm.SHA1));
assertEquals(46119246, TokenCalculator.TOTP_RFC6238(keySHA256, TOTP_DEFAULT_PERIOD, 59L, 8, TokenCalculator.HashAlgorithm.SHA256));
assertEquals(90693936, TokenCalculator.TOTP_RFC6238(keySHA512, TOTP_DEFAULT_PERIOD, 59L, 8, TokenCalculator.HashAlgorithm.SHA512));
assertEquals(94287082, TokenCalculator.TOTP_RFC6238(keySHA1, TokenCalculator.TOTP_DEFAULT_PERIOD, 59L, 8, TokenCalculator.HashAlgorithm.SHA1));
assertEquals(46119246, TokenCalculator.TOTP_RFC6238(keySHA256, TokenCalculator.TOTP_DEFAULT_PERIOD, 59L, 8, TokenCalculator.HashAlgorithm.SHA256));
assertEquals(90693936, TokenCalculator.TOTP_RFC6238(keySHA512, TokenCalculator.TOTP_DEFAULT_PERIOD, 59L, 8, TokenCalculator.HashAlgorithm.SHA512));
assertEquals(7081804, TokenCalculator.TOTP_RFC6238(keySHA1, TOTP_DEFAULT_PERIOD, 1111111109L, 8, TokenCalculator.HashAlgorithm.SHA1));
assertEquals(68084774, TokenCalculator.TOTP_RFC6238(keySHA256, TOTP_DEFAULT_PERIOD, 1111111109L, 8, TokenCalculator.HashAlgorithm.SHA256));
assertEquals(25091201, TokenCalculator.TOTP_RFC6238(keySHA512, TOTP_DEFAULT_PERIOD, 1111111109L, 8, TokenCalculator.HashAlgorithm.SHA512));
assertEquals(7081804, TokenCalculator.TOTP_RFC6238(keySHA1, TokenCalculator.TOTP_DEFAULT_PERIOD, 1111111109L, 8, TokenCalculator.HashAlgorithm.SHA1));
assertEquals(68084774, TokenCalculator.TOTP_RFC6238(keySHA256, TokenCalculator.TOTP_DEFAULT_PERIOD, 1111111109L, 8, TokenCalculator.HashAlgorithm.SHA256));
assertEquals(25091201, TokenCalculator.TOTP_RFC6238(keySHA512, TokenCalculator.TOTP_DEFAULT_PERIOD, 1111111109L, 8, TokenCalculator.HashAlgorithm.SHA512));
assertEquals(14050471, TokenCalculator.TOTP_RFC6238(keySHA1, TOTP_DEFAULT_PERIOD, 1111111111L, 8, TokenCalculator.HashAlgorithm.SHA1));
assertEquals(67062674, TokenCalculator.TOTP_RFC6238(keySHA256, TOTP_DEFAULT_PERIOD, 1111111111L, 8, TokenCalculator.HashAlgorithm.SHA256));
assertEquals(99943326, TokenCalculator.TOTP_RFC6238(keySHA512, TOTP_DEFAULT_PERIOD, 1111111111L, 8, TokenCalculator.HashAlgorithm.SHA512));
assertEquals(14050471, TokenCalculator.TOTP_RFC6238(keySHA1, TokenCalculator.TOTP_DEFAULT_PERIOD, 1111111111L, 8, TokenCalculator.HashAlgorithm.SHA1));
assertEquals(67062674, TokenCalculator.TOTP_RFC6238(keySHA256, TokenCalculator.TOTP_DEFAULT_PERIOD, 1111111111L, 8, TokenCalculator.HashAlgorithm.SHA256));
assertEquals(99943326, TokenCalculator.TOTP_RFC6238(keySHA512, TokenCalculator.TOTP_DEFAULT_PERIOD, 1111111111L, 8, TokenCalculator.HashAlgorithm.SHA512));
assertEquals(89005924, TokenCalculator.TOTP_RFC6238(keySHA1, TOTP_DEFAULT_PERIOD, 1234567890L, 8, TokenCalculator.HashAlgorithm.SHA1));
assertEquals(91819424, TokenCalculator.TOTP_RFC6238(keySHA256, TOTP_DEFAULT_PERIOD, 1234567890L, 8, TokenCalculator.HashAlgorithm.SHA256));
assertEquals(93441116, TokenCalculator.TOTP_RFC6238(keySHA512, TOTP_DEFAULT_PERIOD, 1234567890L, 8, TokenCalculator.HashAlgorithm.SHA512));
assertEquals(89005924, TokenCalculator.TOTP_RFC6238(keySHA1, TokenCalculator.TOTP_DEFAULT_PERIOD, 1234567890L, 8, TokenCalculator.HashAlgorithm.SHA1));
assertEquals(91819424, TokenCalculator.TOTP_RFC6238(keySHA256, TokenCalculator.TOTP_DEFAULT_PERIOD, 1234567890L, 8, TokenCalculator.HashAlgorithm.SHA256));
assertEquals(93441116, TokenCalculator.TOTP_RFC6238(keySHA512, TokenCalculator.TOTP_DEFAULT_PERIOD, 1234567890L, 8, TokenCalculator.HashAlgorithm.SHA512));
assertEquals(69279037, TokenCalculator.TOTP_RFC6238(keySHA1, TOTP_DEFAULT_PERIOD, 2000000000L, 8, TokenCalculator.HashAlgorithm.SHA1));
assertEquals(90698825, TokenCalculator.TOTP_RFC6238(keySHA256, TOTP_DEFAULT_PERIOD, 2000000000L, 8, TokenCalculator.HashAlgorithm.SHA256));
assertEquals(38618901, TokenCalculator.TOTP_RFC6238(keySHA512, TOTP_DEFAULT_PERIOD, 2000000000L, 8, TokenCalculator.HashAlgorithm.SHA512));
assertEquals(69279037, TokenCalculator.TOTP_RFC6238(keySHA1, TokenCalculator.TOTP_DEFAULT_PERIOD, 2000000000L, 8, TokenCalculator.HashAlgorithm.SHA1));
assertEquals(90698825, TokenCalculator.TOTP_RFC6238(keySHA256, TokenCalculator.TOTP_DEFAULT_PERIOD, 2000000000L, 8, TokenCalculator.HashAlgorithm.SHA256));
assertEquals(38618901, TokenCalculator.TOTP_RFC6238(keySHA512, TokenCalculator.TOTP_DEFAULT_PERIOD, 2000000000L, 8, TokenCalculator.HashAlgorithm.SHA512));
assertEquals(65353130, TokenCalculator.TOTP_RFC6238(keySHA1, TOTP_DEFAULT_PERIOD, 20000000000L, 8, TokenCalculator.HashAlgorithm.SHA1));
assertEquals(77737706, TokenCalculator.TOTP_RFC6238(keySHA256, TOTP_DEFAULT_PERIOD, 20000000000L, 8, TokenCalculator.HashAlgorithm.SHA256));
assertEquals(47863826, TokenCalculator.TOTP_RFC6238(keySHA512, TOTP_DEFAULT_PERIOD, 20000000000L, 8, TokenCalculator.HashAlgorithm.SHA512));
assertEquals(65353130, TokenCalculator.TOTP_RFC6238(keySHA1, TokenCalculator.TOTP_DEFAULT_PERIOD, 20000000000L, 8, TokenCalculator.HashAlgorithm.SHA1));
assertEquals(77737706, TokenCalculator.TOTP_RFC6238(keySHA256, TokenCalculator.TOTP_DEFAULT_PERIOD, 20000000000L, 8, TokenCalculator.HashAlgorithm.SHA256));
assertEquals(47863826, TokenCalculator.TOTP_RFC6238(keySHA512, TokenCalculator.TOTP_DEFAULT_PERIOD, 20000000000L, 8, TokenCalculator.HashAlgorithm.SHA512));
}
@ -106,6 +107,8 @@ public class ApplicationTest extends ApplicationTestCase<Application> {
"\"digits\":6," +
"\"type\":\"TOTP\"," +
"\"algorithm\":\"SHA1\"," +
"\"thumbnail\":\"Default\"," +
"\"last_used\":0," +
"\"tags\":[\"test1\",\"test2\"]}";
Entry e = new Entry(new JSONObject(s));
@ -170,10 +173,11 @@ public class ApplicationTest extends ApplicationTestCase<Application> {
keyStore.load(null);
keyStore.deleteEntry("settings");
new File(context.getFilesDir() + "/" + DatabaseHelper.SETTINGS_FILE).delete();
new File(context.getFilesDir() + "/" + DatabaseHelper.KEY_FILE).delete();
new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE).delete();
new File(context.getFilesDir() + "/" + Constants.FILENAME_ENCRYPTED_KEY).delete();
ArrayList<Entry> b = DatabaseHelper.loadDatabase(context);
SecretKey encryptionKey = KeyStoreHelper.loadEncryptionKeyFromKeyStore(context);
ArrayList<Entry> b = DatabaseHelper.loadDatabase(context, encryptionKey);
assertEquals(0, b.size());
ArrayList<Entry> a = new ArrayList<>();
@ -187,13 +191,13 @@ public class ApplicationTest extends ApplicationTestCase<Application> {
e.setSecret("secret2".getBytes());
a.add(e);
DatabaseHelper.saveDatabase(context, a);
b = DatabaseHelper.loadDatabase(context);
DatabaseHelper.saveDatabase(context, a, encryptionKey);
b = DatabaseHelper.loadDatabase(context, encryptionKey);
assertEquals(a, b);
new File(context.getFilesDir() + "/" + DatabaseHelper.SETTINGS_FILE).delete();
new File(context.getFilesDir() + "/" + DatabaseHelper.KEY_FILE).delete();
new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE).delete();
new File(context.getFilesDir() + "/" + Constants.FILENAME_ENCRYPTED_KEY).delete();
}
public void testEncryptionHelper() throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, UnsupportedEncodingException, InvalidAlgorithmParameterException, DecoderException {

View file

@ -29,11 +29,13 @@ import android.support.design.widget.TextInputLayout;
import android.support.v7.widget.Toolbar;
import android.text.InputType;
import android.text.method.PasswordTransformationMethod;
import android.util.Base64;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewStub;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
@ -41,13 +43,25 @@ import android.widget.Toast;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.shadowice.flocke.andotp.R;
import org.shadowice.flocke.andotp.Utilities.Constants;
import org.shadowice.flocke.andotp.Utilities.EncryptionHelper;
import static org.shadowice.flocke.andotp.Utilities.Settings.AuthMethod;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import static org.shadowice.flocke.andotp.Utilities.Constants.AuthMethod;
public class AuthenticateActivity extends ThemedActivity
implements EditText.OnEditorActionListener {
implements EditText.OnEditorActionListener, View.OnClickListener {
private String password;
AuthMethod authMethod;
String newEncryption = "";
boolean oldPassword = false;
TextInputEditText passwordInput;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -63,65 +77,114 @@ public class AuthenticateActivity extends ThemedActivity
stub.setLayoutResource(R.layout.content_authenticate);
View v = stub.inflate();
Intent callingIntent = getIntent();
int labelMsg = callingIntent.getIntExtra(Constants.EXTRA_AUTH_MESSAGE, R.string.auth_msg_authenticate);
newEncryption = callingIntent.getStringExtra(Constants.EXTRA_AUTH_NEW_ENCRYPTION);
TextView passwordLabel = v.findViewById(R.id.passwordLabel);
TextInputLayout passwordLayout = v.findViewById(R.id.passwordLayout);
TextInputEditText passwordInput = v.findViewById(R.id.passwordEdit);
passwordInput = v.findViewById(R.id.passwordEdit);
AuthMethod authMethod = settings.getAuthMethod();
passwordLabel.setText(labelMsg);
if (authMethod == AuthMethod.PASSWORD) {
password = settings.getAuthPasswordHash();
authMethod = settings.getAuthMethod();
password = settings.getAuthCredentials();
if (password.isEmpty()) {
password = settings.getOldCredentials(authMethod);
oldPassword = true;
}
if (authMethod == AuthMethod.PASSWORD) {
if (password.isEmpty()) {
Toast.makeText(this, R.string.auth_toast_password_missing, Toast.LENGTH_LONG).show();
finishWithResult(true);
finishWithResult(true, null);
} else {
passwordLabel.setText(R.string.auth_msg_password);
passwordLayout.setHint(getString(R.string.auth_hint_password));
passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
}
} else if (authMethod == AuthMethod.PIN) {
password = settings.getAuthPINHash();
if (password.isEmpty()) {
Toast.makeText(this, R.string.auth_toast_pin_missing, Toast.LENGTH_LONG).show();
finishWithResult(true);
finishWithResult(true, null);
} else {
passwordLabel.setText(R.string.auth_msg_pin);
passwordLayout.setHint(getString(R.string.auth_hint_pin));
passwordInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
}
} else {
finishWithResult(true);
finishWithResult(true, null);
}
passwordInput.setTransformationMethod(new PasswordTransformationMethod());
passwordInput.setOnEditorActionListener(this);
Button unlockButton = v.findViewById(R.id.buttonUnlock);
unlockButton.setOnClickListener(this);
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
}
@Override
public void onClick(View view) {
checkPassword(passwordInput.getText().toString());
}
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
String hashedPassword = new String(Hex.encodeHex(DigestUtils.sha256(v.getText().toString())));
if (hashedPassword.equals(password)) {
finishWithResult(true);
} else {
finishWithResult(false);
}
checkPassword(v.getText().toString());
return true;
}
return false;
}
public void checkPassword(String plainPassword) {
if (! oldPassword) {
try {
EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(plainPassword, settings.getSalt(), settings.getIterations());
byte[] passwordArray = Base64.decode(password, Base64.URL_SAFE);
if (Arrays.equals(passwordArray, credentials.password)) {
finishWithResult(true, credentials.key);
} else {
finishWithResult(false, null);
}
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
e.printStackTrace();
finishWithResult(false, null);
}
} else {
String hashedPassword = new String(Hex.encodeHex(DigestUtils.sha256(plainPassword)));
if (hashedPassword.equals(password)) {
byte[] key = settings.setAuthCredentials(password);
if (key == null)
Toast.makeText(this, R.string.settings_toast_auth_upgrade_failed, Toast.LENGTH_LONG).show();
if (authMethod == AuthMethod.PASSWORD)
settings.removeAuthPasswordHash();
else if (authMethod == AuthMethod.PIN)
settings.removeAuthPINHash();
finishWithResult(true, key);
} else {
finishWithResult(false, null);
}
}
}
// End with a result
public void finishWithResult(boolean success) {
public void finishWithResult(boolean success, byte[] key) {
Intent data = new Intent();
if (newEncryption != null && ! newEncryption.isEmpty())
data.putExtra(Constants.EXTRA_AUTH_NEW_ENCRYPTION, newEncryption);
if (key != null)
data.putExtra(Constants.EXTRA_AUTH_PASSWORD_KEY, key);
if (success)
setResult(RESULT_OK, data);
@ -131,7 +194,7 @@ public class AuthenticateActivity extends ThemedActivity
// Go back to the main activity
@Override
public void onBackPressed() {
finishWithResult(false);
finishWithResult(false, null);
super.onBackPressed();
}
}

View file

@ -50,6 +50,7 @@ import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpServiceConnection;
import org.shadowice.flocke.andotp.Database.Entry;
import org.shadowice.flocke.andotp.R;
import org.shadowice.flocke.andotp.Utilities.Constants;
import org.shadowice.flocke.andotp.Utilities.DatabaseHelper;
import org.shadowice.flocke.andotp.Utilities.EncryptionHelper;
import org.shadowice.flocke.andotp.Utilities.FileHelper;
@ -64,28 +65,7 @@ import java.util.ArrayList;
import javax.crypto.SecretKey;
public class BackupActivity extends BaseActivity {
private final static int INTENT_OPEN_DOCUMENT_PLAIN = 200;
private final static int INTENT_SAVE_DOCUMENT_PLAIN = 201;
private final static int INTENT_OPEN_DOCUMENT_CRYPT = 202;
private final static int INTENT_SAVE_DOCUMENT_CRYPT = 203;
private final static int INTENT_OPEN_DOCUMENT_PGP = 204;
private final static int INTENT_SAVE_DOCUMENT_PGP = 205;
private final static int INTENT_ENCRYPT_PGP = 206;
private final static int INTENT_DECRYPT_PGP = 207;
private final static int PERMISSIONS_REQUEST_READ_IMPORT_PLAIN = 210;
private final static int PERMISSIONS_REQUEST_WRITE_EXPORT_PLAIN = 211;
private final static int PERMISSIONS_REQUEST_READ_IMPORT_CRYPT = 212;
private final static int PERMISSIONS_REQUEST_WRITE_EXPORT_CRYPT = 213;
private final static int PERMISSIONS_REQUEST_READ_IMPORT_PGP = 214;
private final static int PERMISSIONS_REQUEST_WRITE_EXPORT_PGP = 215;
private static final String DEFAULT_BACKUP_FILENAME_PLAIN = "otp_accounts.json";
private static final String DEFAULT_BACKUP_FILENAME_CRYPT = "otp_accounts.json.aes";
private static final String DEFAULT_BACKUP_FILENAME_PGP = "otp_accounts.json.gpg";
private static final String DEFAULT_BACKUP_MIMETYPE_PLAIN = "application/json";
private static final String DEFAULT_BACKUP_MIMETYPE_CRYPT = "binary/aes";
private static final String DEFAULT_BACKUP_MIMETYPE_PGP = "application/pgp-encrypted";
private SecretKey encryptionKey = null;
private OpenPgpServiceConnection pgpServiceConnection;
private long pgpKeyId;
@ -111,6 +91,10 @@ public class BackupActivity extends BaseActivity {
stub.setLayoutResource(R.layout.content_backup);
View v = stub.inflate();
Intent callingIntent = getIntent();
byte[] keyMaterial = callingIntent.getByteArrayExtra(Constants.EXTRA_BACKUP_ENCRYPTION_KEY);
encryptionKey = EncryptionHelper.generateSymmetricKey(keyMaterial);
// Plain-text
LinearLayout backupPlain = v.findViewById(R.id.button_backup_plain);
@ -126,7 +110,7 @@ public class BackupActivity extends BaseActivity {
restorePlain.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
openFileWithPermissions(INTENT_OPEN_DOCUMENT_PLAIN, PERMISSIONS_REQUEST_READ_IMPORT_PLAIN);
openFileWithPermissions(Constants.INTENT_BACKUP_OPEN_DOCUMENT_PLAIN, Constants.PERMISSIONS_BACKUP_READ_IMPORT_PLAIN);
}
});
@ -149,14 +133,14 @@ public class BackupActivity extends BaseActivity {
backupCrypt.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
saveFileWithPermissions(DEFAULT_BACKUP_MIMETYPE_CRYPT, DEFAULT_BACKUP_FILENAME_CRYPT, INTENT_SAVE_DOCUMENT_CRYPT, PERMISSIONS_REQUEST_WRITE_EXPORT_CRYPT);
saveFileWithPermissions(Constants.BACKUP_MIMETYPE_CRYPT, Constants.BACKUP_FILENAME_CRYPT, Constants.INTENT_BACKUP_SAVE_DOCUMENT_CRYPT, Constants.PERMISSIONS_BACKUP_WRITE_EXPORT_CRYPT);
}
});
restoreCrypt.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
openFileWithPermissions(INTENT_OPEN_DOCUMENT_CRYPT, PERMISSIONS_REQUEST_READ_IMPORT_CRYPT);
openFileWithPermissions(Constants.INTENT_BACKUP_OPEN_DOCUMENT_CRYPT, Constants.PERMISSIONS_BACKUP_READ_IMPORT_CRYPT);
}
});
@ -184,14 +168,14 @@ public class BackupActivity extends BaseActivity {
backupPGP.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
saveFileWithPermissions(DEFAULT_BACKUP_MIMETYPE_PGP, DEFAULT_BACKUP_FILENAME_PGP, INTENT_SAVE_DOCUMENT_PGP, PERMISSIONS_REQUEST_WRITE_EXPORT_PGP);
saveFileWithPermissions(Constants.BACKUP_MIMETYPE_PGP, Constants.BACKUP_FILENAME_PGP, Constants.INTENT_BACKUP_SAVE_DOCUMENT_PGP, Constants.PERMISSIONS_BACKUP_WRITE_EXPORT_PGP);
}
});
restorePGP.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
openFileWithPermissions(INTENT_OPEN_DOCUMENT_PGP, PERMISSIONS_REQUEST_READ_IMPORT_PGP);
openFileWithPermissions(Constants.INTENT_BACKUP_OPEN_DOCUMENT_PGP, Constants.PERMISSIONS_BACKUP_READ_IMPORT_PGP);
}
});
}
@ -232,39 +216,39 @@ public class BackupActivity extends BaseActivity {
// Get the result from permission requests
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
if (requestCode == PERMISSIONS_REQUEST_READ_IMPORT_PLAIN) {
if (requestCode == Constants.PERMISSIONS_BACKUP_READ_IMPORT_PLAIN) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
showOpenFileSelector(INTENT_OPEN_DOCUMENT_PLAIN);
showOpenFileSelector(Constants.INTENT_BACKUP_OPEN_DOCUMENT_PLAIN);
} else {
Toast.makeText(this, R.string.backup_toast_storage_permissions, Toast.LENGTH_LONG).show();
}
} else if (requestCode == PERMISSIONS_REQUEST_WRITE_EXPORT_PLAIN) {
} else if (requestCode == Constants.PERMISSIONS_BACKUP_WRITE_EXPORT_PLAIN) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
showSaveFileSelector(DEFAULT_BACKUP_MIMETYPE_PLAIN, DEFAULT_BACKUP_FILENAME_PLAIN, INTENT_SAVE_DOCUMENT_PLAIN);
showSaveFileSelector(Constants.BACKUP_MIMETYPE_PLAIN, Constants.BACKUP_FILENAME_PLAIN, Constants.INTENT_BACKUP_SAVE_DOCUMENT_PLAIN);
} else {
Toast.makeText(this, R.string.backup_toast_storage_permissions, Toast.LENGTH_LONG).show();
}
} else if (requestCode == PERMISSIONS_REQUEST_READ_IMPORT_CRYPT) {
} else if (requestCode == Constants.PERMISSIONS_BACKUP_READ_IMPORT_CRYPT) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
showOpenFileSelector(INTENT_OPEN_DOCUMENT_CRYPT);
showOpenFileSelector(Constants.INTENT_BACKUP_OPEN_DOCUMENT_CRYPT);
} else {
Toast.makeText(this, R.string.backup_toast_storage_permissions, Toast.LENGTH_LONG).show();
}
} else if (requestCode == PERMISSIONS_REQUEST_WRITE_EXPORT_CRYPT) {
} else if (requestCode == Constants.PERMISSIONS_BACKUP_WRITE_EXPORT_CRYPT) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
showSaveFileSelector(DEFAULT_BACKUP_MIMETYPE_CRYPT, DEFAULT_BACKUP_FILENAME_CRYPT, INTENT_SAVE_DOCUMENT_CRYPT);
showSaveFileSelector(Constants.BACKUP_MIMETYPE_CRYPT, Constants.BACKUP_FILENAME_CRYPT, Constants.INTENT_BACKUP_SAVE_DOCUMENT_CRYPT);
} else {
Toast.makeText(this, R.string.backup_toast_storage_permissions, Toast.LENGTH_LONG).show();
}
} else if (requestCode == PERMISSIONS_REQUEST_READ_IMPORT_PGP) {
} else if (requestCode == Constants.PERMISSIONS_BACKUP_READ_IMPORT_PGP) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
showOpenFileSelector(INTENT_OPEN_DOCUMENT_PGP);
showOpenFileSelector(Constants.INTENT_BACKUP_OPEN_DOCUMENT_PGP);
} else {
Toast.makeText(this, R.string.backup_toast_storage_permissions, Toast.LENGTH_LONG).show();
}
} else if (requestCode == PERMISSIONS_REQUEST_WRITE_EXPORT_PGP) {
} else if (requestCode == Constants.PERMISSIONS_BACKUP_WRITE_EXPORT_PGP) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
showSaveFileSelector(DEFAULT_BACKUP_MIMETYPE_PGP, DEFAULT_BACKUP_FILENAME_PGP, INTENT_SAVE_DOCUMENT_PGP);
showSaveFileSelector(Constants.BACKUP_MIMETYPE_PGP, Constants.BACKUP_FILENAME_PGP, Constants.INTENT_BACKUP_SAVE_DOCUMENT_PGP);
} else {
Toast.makeText(this, R.string.backup_toast_storage_permissions, Toast.LENGTH_LONG).show();
}
@ -278,31 +262,31 @@ public class BackupActivity extends BaseActivity {
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
if (requestCode == INTENT_OPEN_DOCUMENT_PLAIN && resultCode == RESULT_OK) {
if (requestCode == Constants.INTENT_BACKUP_OPEN_DOCUMENT_PLAIN && resultCode == RESULT_OK) {
if (intent != null) {
doRestorePlain(intent.getData());
}
} else if (requestCode == INTENT_SAVE_DOCUMENT_PLAIN && resultCode == RESULT_OK) {
} else if (requestCode == Constants.INTENT_BACKUP_SAVE_DOCUMENT_PLAIN && resultCode == RESULT_OK) {
if (intent != null) {
doBackupPlain(intent.getData());
}
} else if (requestCode == INTENT_OPEN_DOCUMENT_CRYPT && resultCode == RESULT_OK) {
} else if (requestCode == Constants.INTENT_BACKUP_OPEN_DOCUMENT_CRYPT && resultCode == RESULT_OK) {
if (intent != null) {
doRestoreCrypt(intent.getData());
}
} else if (requestCode == INTENT_SAVE_DOCUMENT_CRYPT && resultCode == RESULT_OK) {
} else if (requestCode == Constants.INTENT_BACKUP_SAVE_DOCUMENT_CRYPT && resultCode == RESULT_OK) {
if (intent != null) {
doBackupCrypt(intent.getData());
}
} else if (requestCode == INTENT_OPEN_DOCUMENT_PGP && resultCode == RESULT_OK) {
} else if (requestCode == Constants.INTENT_BACKUP_OPEN_DOCUMENT_PGP && resultCode == RESULT_OK) {
if (intent != null)
restoreEncryptedWithPGP(intent.getData(), null);
} else if (requestCode == INTENT_SAVE_DOCUMENT_PGP && resultCode == RESULT_OK) {
} else if (requestCode == Constants.INTENT_BACKUP_SAVE_DOCUMENT_PGP && resultCode == RESULT_OK) {
if (intent != null)
backupEncryptedWithPGP(intent.getData(), null);
} else if (requestCode == INTENT_ENCRYPT_PGP && resultCode == RESULT_OK) {
} else if (requestCode == Constants.INTENT_BACKUP_ENCRYPT_PGP && resultCode == RESULT_OK) {
backupEncryptedWithPGP(encryptTargetFile, intent);
} else if (requestCode == INTENT_DECRYPT_PGP && resultCode == RESULT_OK) {
} else if (requestCode == Constants.INTENT_BACKUP_DECRYPT_PGP && resultCode == RESULT_OK) {
restoreEncryptedWithPGP(decryptSourceFile, intent);
}
}
@ -316,12 +300,12 @@ public class BackupActivity extends BaseActivity {
intent.setType("*/*");
startActivityForResult(intent, intentId);
} else {
if (intentId == INTENT_OPEN_DOCUMENT_PLAIN)
doRestorePlain(Tools.buildUri(settings.getBackupDir(), DEFAULT_BACKUP_FILENAME_PLAIN));
else if (intentId == INTENT_OPEN_DOCUMENT_CRYPT)
doRestoreCrypt(Tools.buildUri(settings.getBackupDir(), DEFAULT_BACKUP_FILENAME_CRYPT));
else if (intentId == INTENT_OPEN_DOCUMENT_PGP)
restoreEncryptedWithPGP(Tools.buildUri(settings.getBackupDir(), DEFAULT_BACKUP_FILENAME_PGP), null);
if (intentId == Constants.INTENT_BACKUP_OPEN_DOCUMENT_PLAIN)
doRestorePlain(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_PLAIN));
else if (intentId == Constants.INTENT_BACKUP_OPEN_DOCUMENT_CRYPT)
doRestoreCrypt(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_CRYPT));
else if (intentId == Constants.INTENT_BACKUP_OPEN_DOCUMENT_PGP)
restoreEncryptedWithPGP(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_PGP), null);
}
}
@ -334,12 +318,12 @@ public class BackupActivity extends BaseActivity {
startActivityForResult(intent, intentId);
} else {
if (Tools.mkdir(settings.getBackupDir())) {
if (intentId == INTENT_SAVE_DOCUMENT_PLAIN)
doBackupPlain(Tools.buildUri(settings.getBackupDir(), DEFAULT_BACKUP_FILENAME_PLAIN));
else if (intentId == INTENT_SAVE_DOCUMENT_CRYPT)
doBackupCrypt(Tools.buildUri(settings.getBackupDir(), DEFAULT_BACKUP_FILENAME_CRYPT));
else if (intentId == INTENT_SAVE_DOCUMENT_PGP)
backupEncryptedWithPGP(Tools.buildUri(settings.getBackupDir(), DEFAULT_BACKUP_FILENAME_PGP), null);
if (intentId == Constants.INTENT_BACKUP_SAVE_DOCUMENT_PLAIN)
doBackupPlain(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_PLAIN));
else if (intentId == Constants.INTENT_BACKUP_SAVE_DOCUMENT_CRYPT)
doBackupCrypt(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_CRYPT));
else if (intentId == Constants.INTENT_BACKUP_SAVE_DOCUMENT_PGP)
backupEncryptedWithPGP(Tools.buildUri(settings.getBackupDir(), Constants.BACKUP_FILENAME_PGP), null);
} else {
Toast.makeText(this, R.string.backup_toast_mkdir_failed, Toast.LENGTH_LONG).show();
}
@ -367,13 +351,13 @@ public class BackupActivity extends BaseActivity {
if (entries.size() > 0) {
if (! replace.isChecked()) {
ArrayList<Entry> currentEntries = DatabaseHelper.loadDatabase(this);
ArrayList<Entry> currentEntries = DatabaseHelper.loadDatabase(this, encryptionKey);
entries.removeAll(currentEntries);
entries.addAll(currentEntries);
}
if (DatabaseHelper.saveDatabase(this, entries)) {
if (DatabaseHelper.saveDatabase(this, entries, encryptionKey)) {
reload = true;
Toast.makeText(this, R.string.backup_toast_import_success, Toast.LENGTH_LONG).show();
finishWithResult();
@ -399,9 +383,9 @@ public class BackupActivity extends BaseActivity {
private void doBackupPlain(Uri uri) {
if (Tools.isExternalStorageWritable()) {
boolean success = DatabaseHelper.exportAsJSON(this, uri);
ArrayList<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();
else
Toast.makeText(this, R.string.backup_toast_export_failed, Toast.LENGTH_LONG).show();
@ -420,7 +404,7 @@ public class BackupActivity extends BaseActivity {
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
saveFileWithPermissions(DEFAULT_BACKUP_MIMETYPE_PLAIN, DEFAULT_BACKUP_FILENAME_PLAIN, INTENT_SAVE_DOCUMENT_PLAIN, PERMISSIONS_REQUEST_WRITE_EXPORT_PLAIN);
saveFileWithPermissions(Constants.BACKUP_MIMETYPE_PLAIN, Constants.BACKUP_FILENAME_PLAIN, Constants.INTENT_BACKUP_SAVE_DOCUMENT_PLAIN, Constants.PERMISSIONS_BACKUP_WRITE_EXPORT_PLAIN);
}
})
.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
@ -472,7 +456,7 @@ public class BackupActivity extends BaseActivity {
if (! password.isEmpty()) {
if (Tools.isExternalStorageWritable()) {
ArrayList<Entry> entries = DatabaseHelper.loadDatabase(this);
ArrayList<Entry> entries = DatabaseHelper.loadDatabase(this, encryptionKey);
String plain = DatabaseHelper.entriesToString(entries);
boolean success = true;
@ -515,7 +499,7 @@ public class BackupActivity extends BaseActivity {
ByteArrayOutputStream os = new ByteArrayOutputStream();
OpenPgpApi api = new OpenPgpApi(this, pgpServiceConnection.getService());
Intent result = api.executeApi(decryptIntent, is, os);
handleOpenPGPResult(result, os, uri, INTENT_DECRYPT_PGP);
handleOpenPGPResult(result, os, uri, Constants.INTENT_BACKUP_DECRYPT_PGP);
}
private void doBackupEncrypted(Uri uri, String data) {
@ -534,7 +518,7 @@ public class BackupActivity extends BaseActivity {
}
private void backupEncryptedWithPGP(Uri uri, Intent encryptIntent) {
ArrayList<Entry> entries = DatabaseHelper.loadDatabase(this);
ArrayList<Entry> entries = DatabaseHelper.loadDatabase(this, encryptionKey);
String plainJSON = DatabaseHelper.entriesToString(entries);
if (encryptIntent == null) {
@ -555,7 +539,7 @@ public class BackupActivity extends BaseActivity {
ByteArrayOutputStream os = new ByteArrayOutputStream();
OpenPgpApi api = new OpenPgpApi(this, pgpServiceConnection.getService());
Intent result = api.executeApi(encryptIntent, is, os);
handleOpenPGPResult(result, os, uri, INTENT_ENCRYPT_PGP);
handleOpenPGPResult(result, os, uri, Constants.INTENT_BACKUP_ENCRYPT_PGP);
}
public String outputStreamToString(ByteArrayOutputStream os) {
@ -564,10 +548,10 @@ public class BackupActivity extends BaseActivity {
public void handleOpenPGPResult(Intent result, ByteArrayOutputStream os, Uri file, int requestCode) {
if (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR) == OpenPgpApi.RESULT_CODE_SUCCESS) {
if (requestCode == INTENT_ENCRYPT_PGP) {
if (requestCode == Constants.INTENT_BACKUP_ENCRYPT_PGP) {
if (os != null)
doBackupEncrypted(file, outputStreamToString(os));
} else if (requestCode == INTENT_DECRYPT_PGP) {
} else if (requestCode == Constants.INTENT_BACKUP_DECRYPT_PGP) {
if (os != null) {
if (settings.getOpenPGPVerify()) {
OpenPgpSignatureResult sigResult = result.getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE);
@ -586,9 +570,9 @@ public class BackupActivity extends BaseActivity {
PendingIntent pi = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
// Small hack to keep the target file even after user interaction
if (requestCode == INTENT_ENCRYPT_PGP) {
if (requestCode == Constants.INTENT_BACKUP_ENCRYPT_PGP) {
encryptTargetFile = file;
} else if (requestCode == INTENT_DECRYPT_PGP) {
} else if (requestCode == Constants.INTENT_BACKUP_DECRYPT_PGP) {
decryptSourceFile = file;
}

View file

@ -58,8 +58,9 @@ import com.google.zxing.integration.android.IntentResult;
import org.shadowice.flocke.andotp.Database.Entry;
import org.shadowice.flocke.andotp.R;
import org.shadowice.flocke.andotp.Utilities.DatabaseHelper;
import org.shadowice.flocke.andotp.Utilities.Settings;
import org.shadowice.flocke.andotp.Utilities.Constants;
import org.shadowice.flocke.andotp.Utilities.EncryptionHelper;
import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper;
import org.shadowice.flocke.andotp.Utilities.TokenCalculator;
import org.shadowice.flocke.andotp.View.EntriesCardAdapter;
import org.shadowice.flocke.andotp.View.FloatingActionMenu;
@ -70,13 +71,14 @@ import org.shadowice.flocke.andotp.View.TagsAdapter;
import java.util.ArrayList;
import java.util.HashMap;
import static org.shadowice.flocke.andotp.Utilities.Settings.SortMode;
import javax.crypto.SecretKey;
import static org.shadowice.flocke.andotp.Utilities.Constants.AuthMethod;
import static org.shadowice.flocke.andotp.Utilities.Constants.EncryptionType;
import static org.shadowice.flocke.andotp.Utilities.Constants.SortMode;
public class MainActivity extends BaseActivity
implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final int INTENT_INTERNAL_AUTHENTICATE = 100;
private static final int INTENT_INTERNAL_SETTINGS = 101;
private static final int INTENT_INTERNAL_BACKUP = 102;
private EntriesCardAdapter adapter;
private FloatingActionMenu floatingActionMenu;
@ -84,6 +86,7 @@ public class MainActivity extends BaseActivity
private MenuItem sortMenu;
private SimpleItemTouchHelperCallback touchHelperCallback;
private EncryptionType encryptionType = EncryptionType.KEYSTORE;
private boolean requireAuthentication = false;
private Handler handler;
@ -103,33 +106,51 @@ public class MainActivity extends BaseActivity
private void showFirstTimeWarning() {
ViewGroup container = findViewById(R.id.main_content);
View msgView = getLayoutInflater().inflate(R.layout.dialog_security_backup, container, false);
View msgView = getLayoutInflater().inflate(R.layout.dialog_database_encryption, container, false);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.dialog_title_security_backup)
builder.setTitle(R.string.dialog_title_encryption)
.setView(msgView)
.setPositiveButton(R.string.button_warned, new DialogInterface.OnClickListener() {
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
settings.setFirstTimeWarningShown(true);
updateEncryption(null);
}
})
.setNegativeButton(R.string.button_settings, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
settings.setFirstTimeWarningShown(true);
Intent settingsIntent = new Intent(getBaseContext(), SettingsActivity.class);
startActivityForResult(settingsIntent, Constants.INTENT_MAIN_SETTINGS);
}
})
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialogInterface) {
settings.setFirstTimeWarningShown(true);
updateEncryption(null);
}
})
.create()
.show();
}
public void authenticate() {
Settings.AuthMethod authMethod = settings.getAuthMethod();
public void authenticate(int messageId) {
AuthMethod authMethod = settings.getAuthMethod();
if (authMethod == Settings.AuthMethod.DEVICE) {
if (authMethod == AuthMethod.DEVICE) {
KeyguardManager km = (KeyguardManager) getSystemService(KEYGUARD_SERVICE);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP && km.isKeyguardSecure()) {
Intent authIntent = km.createConfirmDeviceCredentialIntent(getString(R.string.dialog_title_auth), getString(R.string.dialog_msg_auth));
startActivityForResult(authIntent, INTENT_INTERNAL_AUTHENTICATE);
startActivityForResult(authIntent, Constants.INTENT_MAIN_AUTHENTICATE);
}
} else if (authMethod == Settings.AuthMethod.PASSWORD || authMethod == Settings.AuthMethod.PIN) {
} else if (authMethod == AuthMethod.PASSWORD || authMethod == AuthMethod.PIN) {
Intent authIntent = new Intent(this, AuthenticateActivity.class);
startActivityForResult(authIntent, INTENT_INTERNAL_AUTHENTICATE);
authIntent.putExtra(Constants.EXTRA_AUTH_MESSAGE, messageId);
startActivityForResult(authIntent, Constants.INTENT_MAIN_AUTHENTICATE);
}
}
@ -150,6 +171,23 @@ public class MainActivity extends BaseActivity
settings.setSortMode(mode);
}
private HashMap<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
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -168,12 +206,15 @@ public class MainActivity extends BaseActivity
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
settings.registerPreferenceChangeListener(this);
if (savedInstanceState == null)
encryptionType = settings.getEncryption();
if (settings.getAuthMethod() != AuthMethod.NONE && savedInstanceState == null)
requireAuthentication = true;
setBroadcastCallback(new BroadcastReceivedCallback() {
@Override
public void onReceivedScreenOff() {
if (settings.getAuthMethod() != AuthMethod.NONE)
requireAuthentication = true;
}
});
@ -203,16 +244,10 @@ public class MainActivity extends BaseActivity
llm.setOrientation(LinearLayoutManager.VERTICAL);
recList.setLayoutManager(llm);
HashMap<String, Boolean> tagsHashMap = new HashMap<>();
for(Entry entry : DatabaseHelper.loadDatabase(this)) {
for(String tag : entry.getTags())
tagsHashMap.put(tag, settings.getTagToggle(tag));
}
tagsDrawerAdapter = new TagsAdapter(this, tagsHashMap);
tagsDrawerAdapter = new TagsAdapter(this, new HashMap<String, Boolean>());
adapter = new EntriesCardAdapter(this, tagsDrawerAdapter);
recList.setAdapter(adapter);
recList.setAdapter(adapter);
recList.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
@ -293,8 +328,18 @@ public class MainActivity extends BaseActivity
super.onResume();
if (requireAuthentication) {
if (settings.getAuthMethod() != AuthMethod.NONE) {
requireAuthentication = false;
authenticate();
authenticate(R.string.auth_msg_authenticate);
}
} else {
if (settings.getFirstTimeWarningShown()) {
if (adapter.getEncryptionKey() == null) {
updateEncryption(null);
} else {
populateAdapter();
}
}
}
startUpdater();
@ -344,14 +389,20 @@ public class MainActivity extends BaseActivity
Toast.makeText(this, R.string.toast_invalid_qr_code, Toast.LENGTH_LONG).show();
}
}
} else if (requestCode == INTENT_INTERNAL_BACKUP && resultCode == RESULT_OK) {
} else if (requestCode == Constants.INTENT_MAIN_BACKUP && resultCode == RESULT_OK) {
if (intent.getBooleanExtra("reload", false)) {
adapter.loadEntries();
refreshTags();
}
} else if (requestCode == INTENT_INTERNAL_AUTHENTICATE) {
} else if (requestCode == Constants.INTENT_MAIN_SETTINGS && resultCode == RESULT_OK) {
boolean encryptionChanged = intent.getBooleanExtra(Constants.EXTRA_SETTINGS_ENCRYPTION_CHANGED, false);
byte[] newKey = intent.getByteArrayExtra(Constants.EXTRA_SETTINGS_ENCRYPTION_KEY);
if (encryptionChanged)
updateEncryption(newKey);
} else if (requestCode == Constants.INTENT_MAIN_AUTHENTICATE) {
if (resultCode != RESULT_OK) {
Toast.makeText(getBaseContext(), R.string.toast_auth_failed, Toast.LENGTH_LONG).show();
Toast.makeText(getBaseContext(), R.string.toast_auth_failed_fatal, Toast.LENGTH_LONG).show();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
finishAndRemoveTask();
@ -360,10 +411,34 @@ public class MainActivity extends BaseActivity
}
} else {
requireAuthentication = false;
byte[] authKey = intent.getByteArrayExtra(Constants.EXTRA_AUTH_PASSWORD_KEY);
updateEncryption(authKey);
}
}
}
private void updateEncryption(byte[] newKey) {
SecretKey encryptionKey = null;
encryptionType = settings.getEncryption();
if (encryptionType == EncryptionType.KEYSTORE) {
encryptionKey = KeyStoreHelper.loadEncryptionKeyFromKeyStore(this, false);
} else if (encryptionType == EncryptionType.PASSWORD) {
if (newKey != null && newKey.length > 0) {
encryptionKey = EncryptionHelper.generateSymmetricKey(newKey);
} else {
authenticate(R.string.auth_msg_confirm_encryption);
}
}
if (encryptionKey != null)
adapter.setEncryptionKey(encryptionKey);
populateAdapter();
}
// Options menu
@Override
public boolean onCreateOptionsMenu(Menu menu) {
@ -438,10 +513,13 @@ public class MainActivity extends BaseActivity
if (id == R.id.action_backup) {
Intent backupIntent = new Intent(this, BackupActivity.class);
startActivityForResult(backupIntent, INTENT_INTERNAL_BACKUP);
backupIntent.putExtra(Constants.EXTRA_BACKUP_ENCRYPTION_KEY, adapter.getEncryptionKey().getEncoded());
startActivityForResult(backupIntent, Constants.INTENT_MAIN_BACKUP);
} else if (id == R.id.action_settings) {
Intent settingsIntent = new Intent(this, SettingsActivity.class);
startActivityForResult(settingsIntent, INTENT_INTERNAL_SETTINGS);
if (adapter.getEncryptionKey() != null)
settingsIntent.putExtra(Constants.EXTRA_SETTINGS_ENCRYPTION_KEY, adapter.getEncryptionKey().getEncoded());
startActivityForResult(settingsIntent, Constants.INTENT_MAIN_SETTINGS);
} else if (id == R.id.action_about){
Intent aboutIntent = new Intent(this, AboutActivity.class);
startActivity(aboutIntent);

View file

@ -27,11 +27,10 @@ import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import org.shadowice.flocke.andotp.Database.Entry;
import org.shadowice.flocke.andotp.Utilities.DatabaseHelper;
import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper;
import org.shadowice.flocke.andotp.Utilities.Settings;
import java.util.ArrayList;
import java.util.Set;
public class PanicResponderActivity extends Activity {
@ -47,8 +46,10 @@ public class PanicResponderActivity extends Activity {
Set<String> response = settings.getPanicResponse();
if (response.contains("accounts"))
DatabaseHelper.saveDatabase(this, new ArrayList<Entry>());
if (response.contains("accounts")) {
DatabaseHelper.wipeDatabase(this);
KeyStoreHelper.wipeKeys(this);
}
if (response.contains("settings"))
settings.clear(true);

View file

@ -25,6 +25,8 @@ package org.shadowice.flocke.andotp.Activities;
import android.app.KeyguardManager;
import android.app.backup.BackupManager;
import android.app.backup.RestoreObserver;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
@ -40,13 +42,30 @@ import android.widget.Toast;
import org.openintents.openpgp.util.OpenPgpAppPreference;
import org.openintents.openpgp.util.OpenPgpKeyPreference;
import org.shadowice.flocke.andotp.Preferences.PasswordHashPreference;
import org.shadowice.flocke.andotp.Database.Entry;
import org.shadowice.flocke.andotp.Preferences.CredentialsPreference;
import org.shadowice.flocke.andotp.R;
import org.shadowice.flocke.andotp.Utilities.Constants;
import org.shadowice.flocke.andotp.Utilities.DatabaseHelper;
import org.shadowice.flocke.andotp.Utilities.EncryptionHelper;
import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper;
import org.shadowice.flocke.andotp.Utilities.Settings;
import org.shadowice.flocke.andotp.Utilities.UIHelper;
import java.util.ArrayList;
import javax.crypto.SecretKey;
import static org.shadowice.flocke.andotp.Utilities.Constants.AuthMethod;
import static org.shadowice.flocke.andotp.Utilities.Constants.EncryptionType;
public class SettingsActivity extends BaseActivity
implements SharedPreferences.OnSharedPreferenceChangeListener{
SettingsFragment fragment;
SecretKey encryptionKey = null;
boolean encryptionChanged = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -60,6 +79,11 @@ public class SettingsActivity extends BaseActivity
ViewStub stub = findViewById(R.id.container_stub);
stub.inflate();
Intent callingIntent = getIntent();
byte[] keyMaterial = callingIntent.getByteArrayExtra(Constants.EXTRA_SETTINGS_ENCRYPTION_KEY);
if (keyMaterial != null && keyMaterial.length > 0)
encryptionKey = EncryptionHelper.generateSymmetricKey(keyMaterial);
fragment = new SettingsFragment();
getFragmentManager().beginTransaction()
@ -71,7 +95,13 @@ public class SettingsActivity extends BaseActivity
}
public void finishWithResult() {
setResult(RESULT_OK);
Intent data = new Intent();
data.putExtra(Constants.EXTRA_SETTINGS_ENCRYPTION_CHANGED, encryptionChanged);
if (encryptionKey != null)
data.putExtra(Constants.EXTRA_SETTINGS_ENCRYPTION_KEY, encryptionKey.getEncoded());
setResult(RESULT_OK, data);
finish();
}
@ -98,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
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (fragment.pgpKey.handleOnActivityResult(requestCode, resultCode, data)) {
if (requestCode == Constants.INTENT_SETTINGS_AUTHENTICATE) {
if (resultCode == RESULT_OK) {
byte[] authKey = data.getByteArrayExtra(Constants.EXTRA_AUTH_PASSWORD_KEY);
String newEnc = data.getStringExtra(Constants.EXTRA_AUTH_NEW_ENCRYPTION);
if (authKey != null && authKey.length > 0 && newEnc != null && !newEnc.isEmpty()) {
EncryptionType newEncType = EncryptionType.valueOf(newEnc);
tryEncryptionChange(newEncType, authKey);
} else {
Toast.makeText(this, R.string.settings_toast_encryption_no_key, Toast.LENGTH_LONG).show();
}
} else {
Toast.makeText(this, R.string.settings_toast_encryption_auth_failed, Toast.LENGTH_LONG).show();
}
} else if (fragment.pgpKey.handleOnActivityResult(requestCode, resultCode, data)) {
// handled by OpenPgpKeyPreference
return;
}
@ -110,80 +217,57 @@ public class SettingsActivity extends BaseActivity
public static class SettingsFragment extends PreferenceFragment {
PreferenceCategory catSecurity;
Settings settings;
ListPreference encryption;
OpenPgpAppPreference pgpProvider;
OpenPgpKeyPreference pgpKey;
public void updateAuthPassword(String newAuth) {
PasswordHashPreference pwPref = (PasswordHashPreference) catSecurity.findPreference(getString(R.string.settings_key_auth_password_hash));
PasswordHashPreference pinPref = (PasswordHashPreference) catSecurity.findPreference(getString(R.string.settings_key_auth_pin_hash));
if (pwPref != null)
catSecurity.removePreference(pwPref);
if (pinPref != null)
catSecurity.removePreference(pinPref);
switch (newAuth) {
case "password":
PasswordHashPreference authPassword = new PasswordHashPreference(getActivity(), null);
authPassword.setTitle(R.string.settings_title_auth_password);
authPassword.setOrder(4);
authPassword.setKey(getString(R.string.settings_key_auth_password_hash));
authPassword.setMode(PasswordHashPreference.Mode.PASSWORD);
catSecurity.addPreference(authPassword);
break;
case "pin":
PasswordHashPreference authPIN = new PasswordHashPreference(getActivity(), null);
authPIN.setTitle(R.string.settings_title_auth_pin);
authPIN.setOrder(4);
authPIN.setKey(getString(R.string.settings_key_auth_pin_hash));
authPIN.setMode(PasswordHashPreference.Mode.PIN);
catSecurity.addPreference(authPIN);
break;
default:
break;
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getActivity().getBaseContext());
settings = new Settings(getActivity());
final SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getActivity().getBaseContext());
addPreferencesFromResource(R.xml.preferences);
CredentialsPreference credentialsPreference = (CredentialsPreference) findPreference(getString(R.string.settings_key_auth));
credentialsPreference.setEncryptionChangeCallback(new CredentialsPreference.EncryptionChangeCallback() {
@Override
public boolean testEncryptionChange(byte[] newKey) {
return ((SettingsActivity) getActivity()).tryEncryptionChange(settings.getEncryption(), newKey);
}
});
// Authentication
catSecurity = (PreferenceCategory) findPreference(getString(R.string.settings_key_cat_security));
ListPreference authPref = (ListPreference) findPreference(getString(R.string.settings_key_auth));
encryption = (ListPreference) findPreference(getString(R.string.settings_key_encryption));
updateAuthPassword(authPref.getValue());
authPref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
encryption.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object o) {
String newAuth = (String) o;
public boolean onPreferenceChange(final Preference preference, Object o) {
String newEncryption = (String) o;
EncryptionType encryptionType = EncryptionType.valueOf(newEncryption.toUpperCase());
AuthMethod authMethod = settings.getAuthMethod();
if (newAuth.equals("device")) {
KeyguardManager km = (KeyguardManager) getActivity().getSystemService(KEYGUARD_SERVICE);
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
Toast.makeText(getActivity(), R.string.settings_toast_auth_device_pre_lollipop, Toast.LENGTH_LONG).show();
if (encryptionType == EncryptionType.PASSWORD) {
if (authMethod != AuthMethod.PASSWORD && authMethod != AuthMethod.PIN) {
UIHelper.showGenericDialog(getActivity(), R.string.settings_dialog_title_error, R.string.settings_dialog_msg_encryption_invalid_with_auth);
return false;
} else if (! km.isKeyguardSecure()) {
Toast.makeText(getActivity(), R.string.settings_toast_auth_device_not_secure, Toast.LENGTH_LONG).show();
} else {
if (settings.getAuthCredentials().isEmpty()) {
UIHelper.showGenericDialog(getActivity(), R.string.settings_dialog_title_error, R.string.settings_dialog_msg_encryption_invalid_without_credentials);
return false;
}
}
updateAuthPassword(newAuth);
((SettingsActivity) getActivity()).tryEncryptionChangeWithAuth(encryptionType);
} else if (encryptionType == EncryptionType.KEYSTORE) {
((SettingsActivity) getActivity()).tryEncryptionChange(encryptionType, null);
}
return true;
return false;
}
});
@ -205,6 +289,40 @@ public class SettingsActivity extends BaseActivity
if (sharedPref.contains(getString(R.string.settings_key_special_features)) &&
sharedPref.getBoolean(getString(R.string.settings_key_special_features), false)) {
addPreferencesFromResource(R.xml.preferences_special);
Preference clearKeyStore = findPreference(getString(R.string.settings_key_clear_keystore));
clearKeyStore.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.settings_dialog_title_clear_keystore);
if (settings.getEncryption() == EncryptionType.PASSWORD)
builder.setMessage(R.string.settings_dialog_msg_clear_keystore_password);
else if (settings.getEncryption() == EncryptionType.KEYSTORE)
builder.setMessage(R.string.settings_dialog_msg_clear_keystore_keystore);
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
KeyStoreHelper.wipeKeys(getActivity());
if (settings.getEncryption() == EncryptionType.KEYSTORE) {
DatabaseHelper.wipeDatabase(getActivity());
((SettingsActivity) getActivity()).generateNewEncryptionKey();
}
}
});
builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
}
});
builder.create().show();
return false;
}
});
}
}
}

View file

@ -41,7 +41,9 @@ import java.util.Objects;
import java.util.Set;
public class Entry {
public enum OTPType { TOTP, STEAM}
public enum OTPType {
TOTP, STEAM
}
public static Set<OTPType> PublicTypes = EnumSet.of(OTPType.TOTP);
private static final OTPType DEFAULT_TYPE = OTPType.TOTP;

View file

@ -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) {}
}

View file

@ -39,6 +39,7 @@ import android.widget.Button;
import android.widget.EditText;
import org.shadowice.flocke.andotp.R;
import org.shadowice.flocke.andotp.Utilities.Constants;
import org.shadowice.flocke.andotp.Utilities.EncryptionHelper;
import org.shadowice.flocke.andotp.Utilities.KeyStoreHelper;
@ -52,7 +53,6 @@ public class PasswordEncryptedPreference extends DialogPreference
PASSWORD, PIN
}
public static final String KEY_ALIAS = "password";
private KeyPair key;
private static final String DEFAULT_VALUE = "";
@ -70,7 +70,7 @@ public class PasswordEncryptedPreference extends DialogPreference
super(context, attrs);
try {
key = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, KEY_ALIAS);
key = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, Constants.KEYSTORE_ALIAS_PASSWORD);
} catch (Exception e) {
e.printStackTrace();
}

View file

@ -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) {}
}

View file

@ -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";
}

View file

@ -25,35 +25,90 @@ package org.shadowice.flocke.andotp.Utilities;
import android.app.backup.BackupManager;
import android.content.Context;
import android.net.Uri;
import android.widget.Toast;
import org.json.JSONArray;
import org.shadowice.flocke.andotp.Database.Entry;
import org.shadowice.flocke.andotp.R;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import javax.crypto.SecretKey;
public class DatabaseHelper {
public static final String KEY_FILE = "otp.key";
public static final String SETTINGS_FILE = "secrets.dat";
static final Object DatabaseFileLock = new Object();
/* Database functions */
public static void wipeDatabase(Context context) {
File db = new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE);
File dbBackup = new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE_BACKUP);
db.delete();
dbBackup.delete();
}
private static void copyFile(File src, File dst)
throws IOException {
try (InputStream in = new FileInputStream(src)) {
try (OutputStream out = new FileOutputStream(dst)) {
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
}
}
}
public static boolean backupDatabase(Context context) {
File original = new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE);
File backup = new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE_BACKUP);
if (original.exists()) {
try {
copyFile(original, backup);
} catch (IOException e) {
return false;
}
}
return true;
}
public static boolean restoreDatabaseBackup(Context context) {
File original = new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE);
File backup = new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE_BACKUP);
if (backup.exists()) {
try {
copyFile(backup, original);
} catch (IOException e) {
return false;
}
}
return true;
}
/* Database functions */
public static boolean saveDatabase(Context context, ArrayList<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);
try {
synchronized (DatabaseHelper.DatabaseFileLock) {
byte[] data = jsonString.getBytes();
byte[] data = EncryptionHelper.encrypt(encryptionKey, jsonString.getBytes());
SecretKey key = KeyStoreHelper.loadOrGenerateWrappedKey(context, new File(context.getFilesDir() + "/" + KEY_FILE));
data = EncryptionHelper.encrypt(key, data);
FileHelper.writeBytesToFile(new File(context.getFilesDir() + "/" + SETTINGS_FILE), data);
FileHelper.writeBytesToFile(new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE), data);
}
} catch (Exception error) {
error.printStackTrace();
@ -66,21 +121,23 @@ public class DatabaseHelper {
return true;
}
public static ArrayList<Entry> loadDatabase(Context context){
public static ArrayList<Entry> loadDatabase(Context context, SecretKey encryptionKey) {
ArrayList<Entry> entries = new ArrayList<>();
if (encryptionKey != null) {
try {
synchronized (DatabaseHelper.DatabaseFileLock) {
byte[] data = FileHelper.readFileToBytes(new File(context.getFilesDir() + "/" + SETTINGS_FILE));
SecretKey key = KeyStoreHelper.loadOrGenerateWrappedKey(context, new File(context.getFilesDir() + "/" + KEY_FILE));
data = EncryptionHelper.decrypt(key, data);
byte[] data = FileHelper.readFileToBytes(new File(context.getFilesDir() + "/" + Constants.FILENAME_DATABASE));
data = EncryptionHelper.decrypt(encryptionKey, data);
entries = stringToEntries(new String(data));
}
} catch (Exception error) {
error.printStackTrace();
}
} else {
Toast.makeText(context, R.string.toast_encryption_key_empty, Toast.LENGTH_LONG).show();
}
return entries;
}
@ -117,12 +174,4 @@ public class DatabaseHelper {
return entries;
}
/* Export functions */
public static boolean exportAsJSON(Context context, Uri file) {
ArrayList<Entry> entries = loadDatabase(context);
return FileHelper.writeStringToFile(context, file, entriesToString(entries));
}
}

View file

@ -23,30 +23,71 @@
package org.shadowice.flocke.andotp.Utilities;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Random;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class EncryptionHelper {
private final static String ALGORITHM_SYMMETRIC = "AES/GCM/NoPadding";
private final static String ALGORITHM_ASYMMETRIC = "RSA/ECB/PKCS1Padding";
public static class PBKDF2Credentials {
public byte[] password;
public byte[] key;
}
private final static int IV_LENGTH = 12;
public static int generateRandomIterations() {
Random rand = new Random();
return rand.nextInt((Constants.PBKDF2_MAX_ITERATIONS - Constants.PBKDF2_MIN_ITERATIONS) + 1) + Constants.PBKDF2_MIN_ITERATIONS;
}
public static byte[] generateRandom(int length) {
final byte[] raw = new byte[length];
new SecureRandom().nextBytes(raw);
return raw;
}
public static PBKDF2Credentials generatePBKDF2Credentials(String password, byte[] salt, int iter)
throws NoSuchAlgorithmException, InvalidKeySpecException {
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, iter, Constants.PBKDF2_LENGTH);
byte[] array = secretKeyFactory.generateSecret(keySpec).getEncoded();
int halfPoint = array.length / 2;
PBKDF2Credentials credentials = new PBKDF2Credentials();
credentials.password = Arrays.copyOfRange(array, halfPoint, array.length);
credentials.key = Arrays.copyOfRange(array, 0, halfPoint);
return credentials;
}
public static SecretKey generateSymmetricKey(byte[] data) {
return new SecretKeySpec(data, 0, data.length, "AES");
}
public static SecretKey generateSymmetricKeyFromPassword(String password)
throws NoSuchAlgorithmException {
@ -57,7 +98,7 @@ public class EncryptionHelper {
public static byte[] encrypt(SecretKey secretKey, IvParameterSpec iv, byte[] plainText)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(ALGORITHM_SYMMETRIC);
Cipher cipher = Cipher.getInstance(Constants.ALGORITHM_SYMMETRIC);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
return cipher.doFinal(plainText);
@ -65,7 +106,7 @@ public class EncryptionHelper {
public static byte[] encrypt(SecretKey secretKey, byte[] plaintext)
throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, UnsupportedEncodingException, InvalidAlgorithmParameterException {
final byte[] iv = new byte[IV_LENGTH];
final byte[] iv = new byte[Constants.ENCRYPTION_IV_LENGTH];
new SecureRandom().nextBytes(iv);
byte[] cipherText = encrypt(secretKey, new IvParameterSpec(iv), plaintext);
@ -79,7 +120,7 @@ public class EncryptionHelper {
public static byte[] encrypt(PublicKey publicKey, byte[] plaintext)
throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, UnsupportedEncodingException, InvalidAlgorithmParameterException {
Cipher cipher = Cipher.getInstance(ALGORITHM_ASYMMETRIC);
Cipher cipher = Cipher.getInstance(Constants.ALGORITHM_ASYMMETRIC);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(plaintext);
@ -87,7 +128,7 @@ public class EncryptionHelper {
public static byte[] decrypt(SecretKey secretKey, IvParameterSpec iv, byte[] cipherText)
throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
Cipher cipher = Cipher.getInstance(ALGORITHM_SYMMETRIC);
Cipher cipher = Cipher.getInstance(Constants.ALGORITHM_SYMMETRIC);
cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
return cipher.doFinal(cipherText);
@ -95,17 +136,44 @@ public class EncryptionHelper {
public static byte[] decrypt(SecretKey secretKey, byte[] cipherText)
throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
byte[] iv = Arrays.copyOfRange(cipherText, 0, IV_LENGTH);
byte[] encrypted = Arrays.copyOfRange(cipherText, IV_LENGTH, cipherText.length);
byte[] iv = Arrays.copyOfRange(cipherText, 0, Constants.ENCRYPTION_IV_LENGTH);
byte[] encrypted = Arrays.copyOfRange(cipherText, Constants.ENCRYPTION_IV_LENGTH, cipherText.length);
return decrypt(secretKey, new IvParameterSpec(iv), encrypted);
}
public static byte[] decrypt(PrivateKey privateKey, byte[] cipherText)
throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
Cipher cipher = Cipher.getInstance(ALGORITHM_ASYMMETRIC);
Cipher cipher = Cipher.getInstance(Constants.ALGORITHM_ASYMMETRIC);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(cipherText);
}
/**
* Load our symmetric secret key.
* The symmetric secret key is stored securely on disk by wrapping
* it with a public/private key pair, possibly backed by hardware.
*/
public static SecretKey loadOrGenerateWrappedKey(File keyFile, KeyPair keyPair)
throws GeneralSecurityException, IOException {
final SecretKeyWrapper wrapper = new SecretKeyWrapper(keyPair);
// Generate secret key if none exists
if (!keyFile.exists()) {
final byte[] raw = EncryptionHelper.generateRandom(Constants.ENCRYPTION_KEY_LENGTH);
final SecretKey key = new SecretKeySpec(raw, "AES");
final byte[] wrapped = wrapper.wrap(key);
FileHelper.writeBytesToFile(keyFile, wrapped);
}
// Even if we just generated the key, always read it back to ensure we
// can read it successfully.
final byte[] wrapped = FileHelper.readFileToBytes(keyFile);
return wrapper.unwrap(wrapped);
}
}

View file

@ -28,6 +28,8 @@ import android.security.KeyPairGeneratorSpec;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import org.shadowice.flocke.andotp.R;
import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
@ -35,17 +37,29 @@ import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.ProviderException;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Calendar;
import java.util.GregorianCalendar;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.security.auth.x500.X500Principal;
public class KeyStoreHelper {
private final static int KEY_LENGTH = 16;
public static void wipeKeys(Context context) {
File keyFile = new File(context.getFilesDir() + "/" + Constants.FILENAME_ENCRYPTED_KEY);
keyFile.delete();
try {
final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
if (keyStore.containsAlias(Constants.KEYSTORE_ALIAS_WRAPPING))
keyStore.deleteEntry(Constants.KEYSTORE_ALIAS_WRAPPING);
} catch (GeneralSecurityException | IOException e) {
e.printStackTrace();
}
}
public static KeyPair loadOrGenerateAsymmetricKeyPair(Context context, String alias)
throws GeneralSecurityException, IOException {
@ -83,34 +97,26 @@ public class KeyStoreHelper {
}
final KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, null);
if (entry != null)
return new KeyPair(entry.getCertificate().getPublicKey(), entry.getPrivateKey());
else
return null;
}
/**
* Load our symmetric secret key.
* The symmetric secret key is stored securely on disk by wrapping
* it with a public/private key pair, possibly backed by hardware.
*/
public static SecretKey loadOrGenerateWrappedKey(Context context, File keyFile)
throws GeneralSecurityException, IOException {
final SecretKeyWrapper wrapper = new SecretKeyWrapper(context, "settings");
public static SecretKey loadEncryptionKeyFromKeyStore(Context context, boolean failSilent) {
SecretKey encKey = null;
// Generate secret key if none exists
if (!keyFile.exists()) {
final byte[] raw = new byte[KEY_LENGTH];
new SecureRandom().nextBytes(raw);
final SecretKey key = new SecretKeySpec(raw, "AES");
final byte[] wrapped = wrapper.wrap(key);
FileHelper.writeBytesToFile(keyFile, wrapped);
try {
KeyPair pair = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, Constants.KEYSTORE_ALIAS_WRAPPING);
if (pair != null)
encKey = EncryptionHelper.loadOrGenerateWrappedKey(new File(context.getFilesDir() + "/" + Constants.FILENAME_ENCRYPTED_KEY), pair);
} catch (GeneralSecurityException | IOException | ProviderException e) {
e.printStackTrace();
if (! failSilent)
UIHelper.showGenericDialog(context, R.string.dialog_title_keystore_error, R.string.dialog_msg_keystore_error);
}
// Even if we just generated the key, always read it back to ensure we
// can read it successfully.
final byte[] wrapped = FileHelper.readFileToBytes(keyFile);
return wrapper.unwrap(wrapped);
return encKey;
}
}

View file

@ -17,7 +17,6 @@
package org.shadowice.flocke.andotp.Utilities;
import android.annotation.SuppressLint;
import android.content.Context;
import java.io.IOException;
import java.security.GeneralSecurityException;
@ -46,10 +45,10 @@ public class SecretKeyWrapper {
* If no pair with that alias exists, it will be generated.
*/
@SuppressLint("GetInstance")
public SecretKeyWrapper(Context context, String alias)
public SecretKeyWrapper(KeyPair keyPair)
throws GeneralSecurityException, IOException {
mCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
mPair = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, alias);
mCipher = Cipher.getInstance(Constants.ALGORITHM_ASYMMETRIC);
mPair = keyPair;
}
/**

View file

@ -24,38 +24,29 @@ package org.shadowice.flocke.andotp.Utilities;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Environment;
import android.preference.PreferenceManager;
import android.util.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.shadowice.flocke.andotp.Preferences.CredentialsPreference;
import org.shadowice.flocke.andotp.R;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import static org.shadowice.flocke.andotp.Preferences.PasswordEncryptedPreference.KEY_ALIAS;
import static org.shadowice.flocke.andotp.Utilities.Constants.AuthMethod;
import static org.shadowice.flocke.andotp.Utilities.Constants.EncryptionType;
import static org.shadowice.flocke.andotp.Utilities.Constants.SortMode;
public class Settings {
private static final String DEFAULT_BACKUP_FOLDER = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "andOTP";
private Context context;
private SharedPreferences settings;
public enum AuthMethod {
NONE, PASSWORD, PIN, DEVICE
}
public enum SortMode {
UNSORTED, LABEL, LAST_USED
}
public Settings(Context context) {
this.context = context;
this.settings = PreferenceManager.getDefaultSharedPreferences(context);
@ -67,26 +58,18 @@ public class Settings {
private void setupDeviceDependedDefaults() {
if (! settings.contains(getResString(R.string.settings_key_backup_directory))
|| settings.getString(getResString(R.string.settings_key_backup_directory), "").isEmpty()) {
setString(R.string.settings_key_backup_directory, DEFAULT_BACKUP_FOLDER);
setString(R.string.settings_key_backup_directory, Constants.BACKUP_FOLDER);
}
}
private void migrateDeprecatedSettings() {
if (settings.contains(getResString(R.string.settings_key_auth_password))) {
String plainPassword = getAuthPassword();
String hashedPassword = new String(Hex.encodeHex(DigestUtils.sha256(plainPassword)));
setString(R.string.settings_key_auth_password_hash, hashedPassword);
setAuthCredentials(getString(R.string.settings_key_auth_password, ""));
remove(R.string.settings_key_auth_password);
}
if (settings.contains(getResString(R.string.settings_key_auth_pin))) {
String plainPIN = getAuthPIN();
String hashedPIN = new String(Hex.encodeHex(DigestUtils.sha256(plainPIN)));
setString(R.string.settings_key_auth_pin_hash, hashedPIN);
setAuthCredentials(getString(R.string.settings_key_auth_pin, ""));
remove(R.string.settings_key_auth_pin);
}
@ -94,7 +77,7 @@ public class Settings {
String plainPassword = getBackupPassword();
try {
KeyPair key = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, KEY_ALIAS);
KeyPair key = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, Constants.KEYSTORE_ALIAS_PASSWORD);
byte[] encPassword = EncryptionHelper.encrypt(key.getPublic(), plainPassword.getBytes(StandardCharsets.UTF_8));
setString(R.string.settings_key_backup_password_enc, Base64.encodeToString(encPassword, Base64.URL_SAFE));
@ -130,6 +113,10 @@ public class Settings {
return settings.getInt(getResString(keyId), getResInt(defaultId));
}
private int getIntValue(int keyId, int defaultValue) {
return settings.getInt(getResString(keyId), defaultValue);
}
private long getLong(int keyId, long defaultValue) {
return settings.getLong(getResString(keyId), defaultValue);
}
@ -144,6 +131,12 @@ public class Settings {
.apply();
}
private void setInt(int keyId, int value) {
settings.edit()
.putInt(getResString(keyId), value)
.apply();
}
private void setString(int keyId, String value) {
settings.edit()
.putString(getResString(keyId), value)
@ -163,9 +156,10 @@ public class Settings {
}
public void clear(boolean keep_auth) {
String authMethod = getAuthMethod().toString().toLowerCase();
String authPassword = getAuthPasswordHash();
String authPIN = getAuthPINHash();
AuthMethod authMethod = getAuthMethod();
String authCredentials = getAuthCredentials();
byte[] authSalt = getSalt();
int authIterations = getIterations();
boolean warningShown = getFirstTimeWarningShown();
@ -175,13 +169,15 @@ public class Settings {
editor.putBoolean(getResString(R.string.settings_key_security_backup_warning), warningShown);
if (keep_auth) {
editor.putString(getResString(R.string.settings_key_auth), authMethod);
editor.putString(getResString(R.string.settings_key_auth), authMethod.toString().toLowerCase());
if (!authPassword.isEmpty())
editor.putString(getResString(R.string.settings_key_auth_password_hash), authPassword);
if (! authCredentials.isEmpty()) {
editor.putString(getResString(R.string.settings_key_auth_credentials), authCredentials);
editor.putInt(getResString(R.string.settings_key_auth_iterations), authIterations);
if (!authPIN.isEmpty())
editor.putString(getResString(R.string.settings_key_auth_pin_hash), authPIN);
String encodedSalt = Base64.encodeToString(authSalt, Base64.URL_SAFE);
editor.putString(getResString(R.string.settings_key_auth_salt), encodedSalt);
}
}
editor.commit();
@ -206,24 +202,82 @@ public class Settings {
}
public AuthMethod getAuthMethod() {
String authString = getString(R.string.settings_key_auth, R.string.settings_default_auth);
String authString = getString(R.string.settings_key_auth, CredentialsPreference.DEFAULT_VALUE.name().toLowerCase());
return AuthMethod.valueOf(authString.toUpperCase());
}
public String getAuthPassword() {
return getString(R.string.settings_key_auth_password, "");
public void removeAuthPasswordHash() {
remove(R.string.settings_key_auth_password_hash);
}
public void removeAuthPINHash() {
remove(R.string.settings_key_auth_pin_hash);
}
public String getAuthPasswordHash() {
public String getOldCredentials(AuthMethod method) {
if (method == AuthMethod.PASSWORD)
return getString(R.string.settings_key_auth_password_hash, "");
}
public String getAuthPIN() {
return getString(R.string.settings_key_auth_pin, "");
}
public String getAuthPINHash() {
else if (method == AuthMethod.PIN)
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() {
@ -281,7 +335,7 @@ public class Settings {
}
public String getBackupDir() {
return getString(R.string.settings_key_backup_directory, DEFAULT_BACKUP_FOLDER);
return getString(R.string.settings_key_backup_directory, Constants.BACKUP_FOLDER);
}
public String getBackupPassword() {
@ -295,7 +349,7 @@ public class Settings {
String password = "";
try {
KeyPair key = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, KEY_ALIAS);
KeyPair key = KeyStoreHelper.loadOrGenerateAsymmetricKeyPair(context, Constants.KEYSTORE_ALIAS_PASSWORD);
password = new String(EncryptionHelper.decrypt(key.getPrivate(), encPassword), StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();

View file

@ -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);
}
}
}

View file

@ -62,7 +62,9 @@ import java.util.HashSet;
import java.util.List;
import java.util.concurrent.Callable;
import static org.shadowice.flocke.andotp.Utilities.Settings.SortMode;
import javax.crypto.SecretKey;
import static org.shadowice.flocke.andotp.Utilities.Constants.SortMode;
public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
implements ItemTouchHelperAdapter, Filterable {
@ -74,6 +76,8 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
private Callback callback;
private List<String> tagsFilter = new ArrayList<>();
private SecretKey encryptionKey = null;
private SortMode sortMode = SortMode.UNSORTED;
private TagsAdapter tagsFilterAdapter;
private Settings settings;
@ -83,8 +87,15 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
this.tagsFilterAdapter = tagsFilterAdapter;
this.settings = new Settings(context);
this.taskHandler = new Handler();
this.entries = new ArrayList<>();
}
loadEntries();
public void setEncryptionKey(SecretKey key) {
encryptionKey = key;
}
public SecretKey getEncryptionKey() {
return encryptionKey;
}
@Override
@ -92,6 +103,10 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
return displayedEntries.size();
}
public ArrayList<Entry> getEntries() {
return entries;
}
public void addEntry(Entry e) {
if (! entries.contains(e)) {
entries.add(e);
@ -112,13 +127,15 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
}
public void saveEntries() {
DatabaseHelper.saveDatabase(context, entries);
DatabaseHelper.saveDatabase(context, entries, encryptionKey);
}
public void loadEntries() {
entries = DatabaseHelper.loadDatabase(context);
if (encryptionKey != null) {
entries = DatabaseHelper.loadDatabase(context, encryptionKey);
entriesChanged();
}
}
public void filterByTags(List<String> tags) {
tagsFilter = tags;
@ -255,7 +272,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
displayedEntries.get(position).setLastUsed(timeStamp);
entries.get(realIndex).setLastUsed(timeStamp);
DatabaseHelper.saveDatabase(context, entries);
DatabaseHelper.saveDatabase(context, entries, encryptionKey);
if (sortMode == SortMode.LAST_USED) {
displayedEntries = sortEntries(displayedEntries);
@ -274,7 +291,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
displayedEntries = new ArrayList<>(entries);
notifyItemMoved(fromPosition, toPosition);
DatabaseHelper.saveDatabase(context, entries);
DatabaseHelper.saveDatabase(context, entries, encryptionKey);
}
return true;
@ -314,7 +331,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
Entry e = entries.get(realIndex);
e.setLabel(newLabel);
DatabaseHelper.saveDatabase(context, entries);
DatabaseHelper.saveDatabase(context, entries, encryptionKey);
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@ -395,7 +412,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
Entry e = entries.get(realIndex);
e.setThumbnail(thumbnail);
DatabaseHelper.saveDatabase(context, entries);
DatabaseHelper.saveDatabase(context, entries, encryptionKey);
notifyItemChanged(pos);
alert.cancel();
}
@ -423,7 +440,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
@Override
public Object call() throws Exception {
entries.get(realPos).setTags(tagsAdapter.getActiveTags());
DatabaseHelper.saveDatabase(context, entries);
DatabaseHelper.saveDatabase(context, entries, encryptionKey);
List<String> inUseTags = getTags();
@ -468,7 +485,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
notifyItemRemoved(pos);
entries.remove(realIndex);
DatabaseHelper.saveDatabase(context, entries);
DatabaseHelper.saveDatabase(context, entries, encryptionKey);
}
})
.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {

View file

@ -43,7 +43,6 @@ import java.util.List;
public class EntryViewHolder extends RecyclerView.ViewHolder
implements ItemTouchHelperViewHolder {
private Context context;
private Callback callback;
private boolean tapToReveal;

View 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>

View file

@ -15,7 +15,7 @@
android:textAlignment="center"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textStyle="bold"
android:text="@string/auth_msg_password"/>
android:text="@string/auth_msg_authenticate"/>
<android.support.design.widget.TextInputLayout
android:id="@+id/passwordLayout"
@ -33,4 +33,12 @@
</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>

View 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>

View file

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

View file

@ -41,7 +41,7 @@
<string name="menu_popup_edit_tags">Upravit štítky</string>
<string name="menu_popup_remove">Smazat</string>
<!-- 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_entry_exists">Tento záznam již existuje</string>
<string name="toast_invalid_qr_code">Neplatný QR kód</string>

View file

@ -41,7 +41,7 @@
<string name="menu_popup_edit_tags">Marker bearbeiten</string>
<string name="menu_popup_remove">Entfernen</string>
<!-- 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_entry_exists">Dieser Eintrag ist bereits vorhanden</string>
<string name="toast_invalid_qr_code">Ungültiger QR-Code</string>

View file

@ -41,7 +41,7 @@
<string name="menu_popup_edit_tags">Editar etiquetas</string>
<string name="menu_popup_remove">Eliminar</string>
<!-- 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_entry_exists">La entrada ya existe</string>
<string name="toast_invalid_qr_code">Código QR inválido</string>

View file

@ -41,7 +41,7 @@
<string name="menu_popup_edit_tags">Éditer les tags</string>
<string name="menu_popup_remove">Supprimer</string>
<!-- Toast messages -->
<string name="toast_auth_failed">
<string name="toast_auth_failed_fatal">
L\'authentification a échoué, fermeture dandOTP !
</string>
<string name="toast_copied_to_clipboard">

View file

@ -41,7 +41,7 @@
<string name="menu_popup_edit_tags">Editar etiquetas</string>
<string name="menu_popup_remove">Eliminar</string>
<!-- 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_entry_exists">Este nome xa existe</string>
<string name="toast_invalid_qr_code">Código QR inválido</string>

View file

@ -41,7 +41,7 @@
<string name="menu_popup_edit_tags">Labels bewerken</string>
<string name="menu_popup_remove">Verwijderen</string>
<!-- 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_entry_exists">Deze item bestaat al</string>
<string name="toast_invalid_qr_code">Ongeldige QR Code</string>

View file

@ -41,7 +41,7 @@
<string name="menu_popup_edit_tags">Edytuj znaczniki</string>
<string name="menu_popup_remove">Usuń</string>
<!-- 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_entry_exists">Taki rekord już istnieje</string>
<string name="toast_invalid_qr_code">Nieprawidłowy kod QR</string>

View file

@ -41,7 +41,7 @@
<string name="menu_popup_edit_tags">Изменить теги</string>
<string name="menu_popup_remove">Убрать</string>
<!-- 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_entry_exists">Эта запись уже существует</string>
<string name="toast_invalid_qr_code">Недопустимый QR-код</string>

View file

@ -41,7 +41,7 @@
<string name="menu_popup_edit_tags">编辑标签</string>
<string name="menu_popup_remove">移除</string>
<!-- 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_entry_exists">该项已存在</string>
<string name="toast_invalid_qr_code">无效二维码</string>

View file

@ -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_timeout" translatable="false">pref_tap_to_reveal_timeout</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_hash" translatable="false">pref_auth_password_hash</string>
<string name="settings_key_auth_pin" translatable="false">pref_auth_pin</string>
<string name="settings_key_auth_pin_hash" translatable="false">pref_auth_pin_hash</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> <!-- Deprecated -->
<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> <!-- 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_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_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_openpgp_provider" translatable="false">pref_openpgp_provider</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_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>
<!-- Default values -->
<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_theme" translatable="false">light</string>
<integer name="settings_default_label_size">18</integer>
@ -61,11 +66,9 @@
</array>
<!-- List values -->
<string-array name="settings_values_auth" translatable="false">
<item>none</item>
<string-array name="settings_values_encryption" translatable="false">
<item>keystore</item>
<item>password</item>
<item>pin</item>
<item>device</item>
</string-array>
<string-array name="settings_values_panic" translatable="false">

View file

@ -7,10 +7,16 @@
<string name="auth_hint_pin">PIN</string>
<!-- Messages -->
<string name="auth_msg_password">Please enter your password to start andOTP.</string>
<string name="auth_msg_pin">Please enter your PIN to start andOTP.</string>
<string name="auth_msg_authenticate">Please authenticate 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 -->
<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_password_again">Wrong password, please try again!</string>
<string name="auth_toast_pin_again">Wrong PIN, please try again!</string>
</resources>

View file

@ -7,7 +7,7 @@
<string name="button_scan_qr">Scan QR-Code</string>
<string name="button_save">Save</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_no_tags">No tags</string>
@ -51,36 +51,49 @@
<string name="menu_popup_remove">Remove</string>
<!-- 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_entry_exists">This entry already exists</string>
<string name="toast_invalid_qr_code">Invalid QR Code</string>
<string name="toast_encryption_key_empty">Encryption key not loaded</string>
<!-- Dialogs -->
<string name="dialog_title_auth">Authenticate</string>
<string name="dialog_title_manual_entry">Enter details</string>
<string name="dialog_title_remove">Remove</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_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_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
only stores it encrypted. Part of the encryption key used for this is stored in the Android
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>
<string name="dialog_msg_security_backup_3rd_party">As a drawback this makes backups of the apps
data a little bit more difficult. If you use 3rd party apps (like Titanium Backup) you only
backup the data files, not the encryption key and as a result such backups become useless.
</string>
<string name="dialog_msg_security_backup_warning">Please only use the internal backup functions
provided by the app to backup your accounts! Anything else WILL lead to data loss.
</string>
<string name="dialog_msg_security_backup_one_time">This message will not be shown again.</string>
<string name="dialog_title_security_keystore">1. Android KeyStore</string>
<string name="dialog_title_security_password">2. Password / PIN</string>
<string name="dialog_msg_security_first">To ensure the security of your accounts this app
only stores them in encrypted data files using one of the following two methods:</string>
<string name="dialog_msg_security_keystore">The KeyStore is a system component of Android for
securely storing cryptographic keys. The advantage of this approach is that the keys are
stored separated from the data files and can be backed by hardware cryptography (if the
hardware supports it). However as the keys are not stored with the apps data this method
prevents external backup solutions (like Titanium) from working. If you choose this method
you will have to rely on the internal backup functions provided by andOTP.</string>
<string name="dialog_msg_security_password">This method will encrypt your data with a key
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
you have to have \"tap to reveal\" enabled or use the copy button.\n\nThis message will not
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>

View file

@ -13,6 +13,7 @@
<string name="settings_title_auth">Authentication</string>
<string name="settings_title_auth_password">Password</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_lang">Language</string>
@ -33,6 +34,7 @@
<string name="settings_title_special_features">Enable special features</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_clear_keystore">Clear KeyStore</string>
<!-- Descriptions -->
<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
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
(disabled by default for security reasons)</string>
<string name="settings_desc_enable_android_backup_service">Enables andOTP to use android\'s
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_clear_keystore">Delete the encryption key from the KeyStore</string>
<!-- Toasts -->
<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
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 -->
<string-array name="settings_entries_auth">
<item>None</item>
@ -75,6 +117,11 @@
<item>Device credentials</item>
</string-array>
<string-array name="settings_entries_encryption">
<item>Android KeyStore</item>
<item>Password / PIN</item>
</string-array>
<string-array name="settings_entries_panic">
<item>Wipe all accounts</item>
<item>Reset app settings</item>
@ -97,9 +144,11 @@
<string name="settings_lang_sys_default">System default</string>
<!-- PasswordPreference -->
<string name="settings_hint_password">Password</string>
<string name="settings_hint_pin">PIN</string>
<string name="settings_hint_password">Enter new password</string>
<string name="settings_hint_pin">Enter new PIN</string>
<string name="settings_hint_password_confirm">Confirm password</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>

View file

@ -9,14 +9,12 @@
<CheckBoxPreference
android:key="@string/settings_key_tap_to_reveal"
android:order="1"
android:title="@string/settings_title_tap_to_reveal"
android:summary="@string/settings_desc_tap_to_reveal"
android:defaultValue="false" />
<com.vanniktech.vntnumberpickerpreference.VNTNumberPickerPreference
android:key="@string/settings_key_tap_to_reveal_timeout"
android:order="2"
android:title="@string/settings_title_tap_to_reveal_timeout"
android:dialogMessage="@string/settings_desc_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_maxValue="@integer/settings_max_tap_to_reveal_timeout" />
<ListPreference
<org.shadowice.flocke.andotp.Preferences.CredentialsPreference
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:entries="@array/settings_entries_auth"
android:entryValues="@array/settings_values_auth"
android:defaultValue="@string/settings_default_auth" />
android:entries="@array/settings_entries_encryption"
android:entryValues="@array/settings_values_encryption"
android:defaultValue="@string/settings_default_encryption" />
<MultiSelectListPreference
android:key="@string/settings_key_panic"
android:order="5"
android:title="@string/settings_title_panic"
android:summary="@string/settings_desc_panic"
android:entries="@array/settings_entries_panic"

View file

@ -23,6 +23,11 @@
android:summary="@string/settings_desc_enable_android_backup_service"
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>
</PreferenceScreen>