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) * **Requesting thumbnails**: If you are missing a thumbnail you can request it by editing the [wiki](https://github.com/andOTP/andOTP/wiki/Thumbnails#thumbnail-requests)
* **Discussion and support**: * **Discussion and support**:
- [XDA thread](https://forum.xda-developers.com/android/apps-games/app-andotp-android-otp-authenticator-t3636993) (please keep off-topic to a minimum) - [XDA thread](https://forum.xda-developers.com/android/apps-games/app-andotp-android-otp-authenticator-t3636993) (please keep off-topic to a minimum)
- Telegram channel [@andOTP](https://t.me/andOTP) - Telegram group [@andOTP](https://t.me/andOTP) (also check out the read-only announcement channel for important updates: [@andOTP_Broadcast](https://t.me/andOTP_Broadcast))
#### Contributors: #### Contributors:

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

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:textAlignment="center"
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium"
android:textStyle="bold" android:textStyle="bold"
android:text="@string/auth_msg_password"/> android:text="@string/auth_msg_authenticate"/>
<android.support.design.widget.TextInputLayout <android.support.design.widget.TextInputLayout
android:id="@+id/passwordLayout" android:id="@+id/passwordLayout"
@ -33,4 +33,12 @@
</android.support.design.widget.TextInputLayout> </android.support.design.widget.TextInputLayout>
<Button
android:id="@+id/buttonUnlock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
style="?android:attr/buttonBarButtonStyle"
android:text="@string/auth_button_unlock" />
</LinearLayout> </LinearLayout>

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_edit_tags">Upravit štítky</string>
<string name="menu_popup_remove">Smazat</string> <string name="menu_popup_remove">Smazat</string>
<!-- Toast messages --> <!-- Toast messages -->
<string name="toast_auth_failed">Ověření se nezdařilo, zavírám andOTP!</string> <string name="toast_auth_failed_fatal">Ověření se nezdařilo, zavírám andOTP!</string>
<string name="toast_copied_to_clipboard">Zkopírováno do schránky</string> <string name="toast_copied_to_clipboard">Zkopírováno do schránky</string>
<string name="toast_entry_exists">Tento záznam již existuje</string> <string name="toast_entry_exists">Tento záznam již existuje</string>
<string name="toast_invalid_qr_code">Neplatný QR kód</string> <string name="toast_invalid_qr_code">Neplatný QR kód</string>

View file

@ -41,7 +41,7 @@
<string name="menu_popup_edit_tags">Marker bearbeiten</string> <string name="menu_popup_edit_tags">Marker bearbeiten</string>
<string name="menu_popup_remove">Entfernen</string> <string name="menu_popup_remove">Entfernen</string>
<!-- Toast messages --> <!-- Toast messages -->
<string name="toast_auth_failed">Authentifizierung fehlgeschlagen, andOTP wird geschlossen!</string> <string name="toast_auth_failed_fatal">Authentifizierung fehlgeschlagen, andOTP wird geschlossen!</string>
<string name="toast_copied_to_clipboard">In Zwischenablage kopiert</string> <string name="toast_copied_to_clipboard">In Zwischenablage kopiert</string>
<string name="toast_entry_exists">Dieser Eintrag ist bereits vorhanden</string> <string name="toast_entry_exists">Dieser Eintrag ist bereits vorhanden</string>
<string name="toast_invalid_qr_code">Ungültiger QR-Code</string> <string name="toast_invalid_qr_code">Ungültiger QR-Code</string>

View file

@ -41,7 +41,7 @@
<string name="menu_popup_edit_tags">Editar etiquetas</string> <string name="menu_popup_edit_tags">Editar etiquetas</string>
<string name="menu_popup_remove">Eliminar</string> <string name="menu_popup_remove">Eliminar</string>
<!-- Toast messages --> <!-- Toast messages -->
<string name="toast_auth_failed">La autenticación ha fallado, cerrando andOTP!</string> <string name="toast_auth_failed_fatal">La autenticación ha fallado, cerrando andOTP!</string>
<string name="toast_copied_to_clipboard">Copiado al portapapeles</string> <string name="toast_copied_to_clipboard">Copiado al portapapeles</string>
<string name="toast_entry_exists">La entrada ya existe</string> <string name="toast_entry_exists">La entrada ya existe</string>
<string name="toast_invalid_qr_code">Código QR inválido</string> <string name="toast_invalid_qr_code">Código QR inválido</string>

View file

@ -41,7 +41,7 @@
<string name="menu_popup_edit_tags">Éditer les tags</string> <string name="menu_popup_edit_tags">Éditer les tags</string>
<string name="menu_popup_remove">Supprimer</string> <string name="menu_popup_remove">Supprimer</string>
<!-- Toast messages --> <!-- Toast messages -->
<string name="toast_auth_failed"> <string name="toast_auth_failed_fatal">
L\'authentification a échoué, fermeture dandOTP ! L\'authentification a échoué, fermeture dandOTP !
</string> </string>
<string name="toast_copied_to_clipboard"> <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_edit_tags">Editar etiquetas</string>
<string name="menu_popup_remove">Eliminar</string> <string name="menu_popup_remove">Eliminar</string>
<!-- Toast messages --> <!-- Toast messages -->
<string name="toast_auth_failed">A autenticación fallou, cerrando andOTP!</string> <string name="toast_auth_failed_fatal">A autenticación fallou, cerrando andOTP!</string>
<string name="toast_copied_to_clipboard">Copiado ao portapapeis</string> <string name="toast_copied_to_clipboard">Copiado ao portapapeis</string>
<string name="toast_entry_exists">Este nome xa existe</string> <string name="toast_entry_exists">Este nome xa existe</string>
<string name="toast_invalid_qr_code">Código QR inválido</string> <string name="toast_invalid_qr_code">Código QR inválido</string>

View file

@ -41,7 +41,7 @@
<string name="menu_popup_edit_tags">Labels bewerken</string> <string name="menu_popup_edit_tags">Labels bewerken</string>
<string name="menu_popup_remove">Verwijderen</string> <string name="menu_popup_remove">Verwijderen</string>
<!-- Toast messages --> <!-- Toast messages -->
<string name="toast_auth_failed">Verificatie is mislukt, sluiten andOTP!</string> <string name="toast_auth_failed_fatal">Verificatie is mislukt, sluiten andOTP!</string>
<string name="toast_copied_to_clipboard">Gekopieerd naar klembord</string> <string name="toast_copied_to_clipboard">Gekopieerd naar klembord</string>
<string name="toast_entry_exists">Deze item bestaat al</string> <string name="toast_entry_exists">Deze item bestaat al</string>
<string name="toast_invalid_qr_code">Ongeldige QR Code</string> <string name="toast_invalid_qr_code">Ongeldige QR Code</string>

View file

@ -41,7 +41,7 @@
<string name="menu_popup_edit_tags">Edytuj znaczniki</string> <string name="menu_popup_edit_tags">Edytuj znaczniki</string>
<string name="menu_popup_remove">Usuń</string> <string name="menu_popup_remove">Usuń</string>
<!-- Toast messages --> <!-- Toast messages -->
<string name="toast_auth_failed">Autoryzacja nie powiodła się, zamykam andOTP!</string> <string name="toast_auth_failed_fatal">Autoryzacja nie powiodła się, zamykam andOTP!</string>
<string name="toast_copied_to_clipboard">Skopiowano do schowka</string> <string name="toast_copied_to_clipboard">Skopiowano do schowka</string>
<string name="toast_entry_exists">Taki rekord już istnieje</string> <string name="toast_entry_exists">Taki rekord już istnieje</string>
<string name="toast_invalid_qr_code">Nieprawidłowy kod QR</string> <string name="toast_invalid_qr_code">Nieprawidłowy kod QR</string>

View file

@ -41,7 +41,7 @@
<string name="menu_popup_edit_tags">Изменить теги</string> <string name="menu_popup_edit_tags">Изменить теги</string>
<string name="menu_popup_remove">Убрать</string> <string name="menu_popup_remove">Убрать</string>
<!-- Toast messages --> <!-- Toast messages -->
<string name="toast_auth_failed">Ошибка аутентификации, andOTP закрывается!</string> <string name="toast_auth_failed_fatal">Ошибка аутентификации, andOTP закрывается!</string>
<string name="toast_copied_to_clipboard">Скопировано в буфер обмена</string> <string name="toast_copied_to_clipboard">Скопировано в буфер обмена</string>
<string name="toast_entry_exists">Эта запись уже существует</string> <string name="toast_entry_exists">Эта запись уже существует</string>
<string name="toast_invalid_qr_code">Недопустимый QR-код</string> <string name="toast_invalid_qr_code">Недопустимый QR-код</string>

View file

@ -41,7 +41,7 @@
<string name="menu_popup_edit_tags">编辑标签</string> <string name="menu_popup_edit_tags">编辑标签</string>
<string name="menu_popup_remove">移除</string> <string name="menu_popup_remove">移除</string>
<!-- Toast messages --> <!-- Toast messages -->
<string name="toast_auth_failed">身份验证失败andOTP 正在关闭!</string> <string name="toast_auth_failed_fatal">身份验证失败andOTP 正在关闭!</string>
<string name="toast_copied_to_clipboard">已复制到剪贴板</string> <string name="toast_copied_to_clipboard">已复制到剪贴板</string>
<string name="toast_entry_exists">该项已存在</string> <string name="toast_entry_exists">该项已存在</string>
<string name="toast_invalid_qr_code">无效二维码</string> <string name="toast_invalid_qr_code">无效二维码</string>

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" translatable="false">pref_tap_to_reveal</string>
<string name="settings_key_tap_to_reveal_timeout" translatable="false">pref_tap_to_reveal_timeout</string> <string name="settings_key_tap_to_reveal_timeout" translatable="false">pref_tap_to_reveal_timeout</string>
<string name="settings_key_auth" translatable="false">pref_auth</string> <string name="settings_key_auth" translatable="false">pref_auth</string>
<string name="settings_key_auth_password" translatable="false">pref_auth_password</string> <string name="settings_key_auth_password" translatable="false">pref_auth_password</string> <!-- Deprecated -->
<string name="settings_key_auth_password_hash" translatable="false">pref_auth_password_hash</string> <string name="settings_key_auth_password_hash" translatable="false">pref_auth_password_hash</string> <!-- Deprecated -->
<string name="settings_key_auth_pin" translatable="false">pref_auth_pin</string> <string name="settings_key_auth_pin" translatable="false">pref_auth_pin</string> <!-- Deprecated -->
<string name="settings_key_auth_pin_hash" translatable="false">pref_auth_pin_hash</string> <string name="settings_key_auth_pin_hash" translatable="false">pref_auth_pin_hash</string> <!-- Deprecated -->
<string name="settings_key_auth_credentials" translatable="false">pref_auth_credentials</string>
<string name="settings_key_auth_iterations" translatable="false">pref_auth_iterations</string>
<string name="settings_key_auth_salt" translatable="false">pref_auth_salt</string>
<string name="settings_key_encryption" translatable="false">pref_encryption</string>
<string name="settings_key_panic" translatable="false">pref_panic</string> <string name="settings_key_panic" translatable="false">pref_panic</string>
<string name="settings_key_lang" translatable="false">pref_lang</string> <string name="settings_key_lang" translatable="false">pref_lang</string>
@ -21,7 +25,7 @@
<string name="settings_key_backup_ask" translatable="false">pref_backup_ask</string> <string name="settings_key_backup_ask" translatable="false">pref_backup_ask</string>
<string name="settings_key_backup_directory" translatable="false">pref_backup_directory</string> <string name="settings_key_backup_directory" translatable="false">pref_backup_directory</string>
<string name="settings_key_backup_password" translatable="false">pref_backup_password</string> <string name="settings_key_backup_password" translatable="false">pref_backup_password</string> <!-- Deprecated -->
<string name="settings_key_backup_password_enc" translatable="false">pref_backup_password_enc</string> <string name="settings_key_backup_password_enc" translatable="false">pref_backup_password_enc</string>
<string name="settings_key_openpgp_provider" translatable="false">pref_openpgp_provider</string> <string name="settings_key_openpgp_provider" translatable="false">pref_openpgp_provider</string>
<string name="settings_key_openpgp_keyid" translatable="false">pref_openpgp_keyid</string> <string name="settings_key_openpgp_keyid" translatable="false">pref_openpgp_keyid</string>
@ -37,12 +41,13 @@
<string name="settings_key_enable_screenshot" translatable="false">pref_enable_screenshot</string> <string name="settings_key_enable_screenshot" translatable="false">pref_enable_screenshot</string>
<string name="settings_key_enable_android_backup_service" translatable="false">pref_enable_android_backup_service</string> <string name="settings_key_enable_android_backup_service" translatable="false">pref_enable_android_backup_service</string>
<string name="settings_key_clear_keystore" translatable="false">pref_clear_keystore</string>
<string name="settings_key_last_used_dialog_shown" translatable="false">pref_last_used_dialog_shown</string> <string name="settings_key_last_used_dialog_shown" translatable="false">pref_last_used_dialog_shown</string>
<!-- Default values --> <!-- Default values -->
<integer name="settings_default_tap_to_reveal_timeout">30</integer> <integer name="settings_default_tap_to_reveal_timeout">30</integer>
<string name="settings_default_auth" translatable="false">none</string> <string name="settings_default_encryption" translatable="false">keystore</string>
<string name="settings_default_lang" translatable="false">system</string> <string name="settings_default_lang" translatable="false">system</string>
<string name="settings_default_theme" translatable="false">light</string> <string name="settings_default_theme" translatable="false">light</string>
<integer name="settings_default_label_size">18</integer> <integer name="settings_default_label_size">18</integer>
@ -61,11 +66,9 @@
</array> </array>
<!-- List values --> <!-- List values -->
<string-array name="settings_values_auth" translatable="false"> <string-array name="settings_values_encryption" translatable="false">
<item>none</item> <item>keystore</item>
<item>password</item> <item>password</item>
<item>pin</item>
<item>device</item>
</string-array> </string-array>
<string-array name="settings_values_panic" translatable="false"> <string-array name="settings_values_panic" translatable="false">

View file

@ -7,10 +7,16 @@
<string name="auth_hint_pin">PIN</string> <string name="auth_hint_pin">PIN</string>
<!-- Messages --> <!-- Messages -->
<string name="auth_msg_password">Please enter your password to start andOTP.</string> <string name="auth_msg_authenticate">Please authenticate to start andOTP!</string>
<string name="auth_msg_pin">Please enter your PIN to start andOTP.</string> <string name="auth_msg_confirm_encryption">Please confirm your authentication to generate the
new encryption key!</string>
<!-- Buttons -->
<string name="auth_button_unlock">Unlock</string>
<!-- Toast messages --> <!-- Toast messages -->
<string name="auth_toast_password_missing">Please set a password in the settings!</string> <string name="auth_toast_password_missing">Please set a password in the settings!</string>
<string name="auth_toast_pin_missing">Please set a PIN in the settings!</string> <string name="auth_toast_pin_missing">Please set a PIN in the settings!</string>
<string name="auth_toast_password_again">Wrong password, please try again!</string>
<string name="auth_toast_pin_again">Wrong PIN, please try again!</string>
</resources> </resources>

View file

@ -7,7 +7,7 @@
<string name="button_scan_qr">Scan QR-Code</string> <string name="button_scan_qr">Scan QR-Code</string>
<string name="button_save">Save</string> <string name="button_save">Save</string>
<string name="button_new_tag">New tag</string> <string name="button_new_tag">New tag</string>
<string name="button_warned">You have been warned!</string> <string name="button_settings">Settings</string>
<string name="button_all_tags">All tags</string> <string name="button_all_tags">All tags</string>
<string name="button_no_tags">No tags</string> <string name="button_no_tags">No tags</string>
@ -51,36 +51,49 @@
<string name="menu_popup_remove">Remove</string> <string name="menu_popup_remove">Remove</string>
<!-- Toast messages --> <!-- Toast messages -->
<string name="toast_auth_failed">Authentication failed, closing andOTP!</string> <string name="toast_auth_failed">Authentication failed, please try again!</string>
<string name="toast_auth_failed_fatal">Authentication failed, closing andOTP!</string>
<string name="toast_copied_to_clipboard">Copied to clipboard</string> <string name="toast_copied_to_clipboard">Copied to clipboard</string>
<string name="toast_entry_exists">This entry already exists</string> <string name="toast_entry_exists">This entry already exists</string>
<string name="toast_invalid_qr_code">Invalid QR Code</string> <string name="toast_invalid_qr_code">Invalid QR Code</string>
<string name="toast_encryption_key_empty">Encryption key not loaded</string>
<!-- Dialogs --> <!-- Dialogs -->
<string name="dialog_title_auth">Authenticate</string> <string name="dialog_title_auth">Authenticate</string>
<string name="dialog_title_manual_entry">Enter details</string> <string name="dialog_title_manual_entry">Enter details</string>
<string name="dialog_title_remove">Remove</string> <string name="dialog_title_remove">Remove</string>
<string name="dialog_title_rename">Rename</string> <string name="dialog_title_rename">Rename</string>
<string name="dialog_title_security_backup">Security and Backups</string>
<string name="dialog_title_last_used">Last used</string> <string name="dialog_title_last_used">Last used</string>
<string name="dialog_title_keystore_error">KeyStore error</string>
<string name="dialog_title_encryption">Database encryption</string>
<string name="dialog_msg_auth">Please enter your device credentials to start andOTP.</string> <string name="dialog_msg_auth">Please enter your device credentials to start andOTP.</string>
<string name="dialog_msg_confirm_delete">Are you sure you want do remove the account \"%1$s\"?</string> <string name="dialog_msg_confirm_delete">Are you sure you want do remove the account \"%1$s\"?</string>
<string name="dialog_msg_security_backup_desc">To keep your account information secure this app <string name="dialog_title_security_keystore">1. Android KeyStore</string>
only stores it encrypted. Part of the encryption key used for this is stored in the Android <string name="dialog_title_security_password">2. Password / PIN</string>
KeyStore system. The advantage of this approach is that the key is kept separate from the
apps data and can be backed by hardware cryptography (if your device supports this). <string name="dialog_msg_security_first">To ensure the security of your accounts this app
</string> only stores them in encrypted data files using one of the following two methods:</string>
<string name="dialog_msg_security_backup_3rd_party">As a drawback this makes backups of the apps <string name="dialog_msg_security_keystore">The KeyStore is a system component of Android for
data a little bit more difficult. If you use 3rd party apps (like Titanium Backup) you only securely storing cryptographic keys. The advantage of this approach is that the keys are
backup the data files, not the encryption key and as a result such backups become useless. stored separated from the data files and can be backed by hardware cryptography (if the
</string> hardware supports it). However as the keys are not stored with the apps data this method
<string name="dialog_msg_security_backup_warning">Please only use the internal backup functions prevents external backup solutions (like Titanium) from working. If you choose this method
provided by the app to backup your accounts! Anything else WILL lead to data loss. you will have to rely on the internal backup functions provided by andOTP.</string>
</string> <string name="dialog_msg_security_password">This method will encrypt your data with a key
<string name="dialog_msg_security_backup_one_time">This message will not be shown again.</string> generated from a password or PIN. The main advantage here is that this will work with
external backup solutions (like Titanium). However you will have to enter your credentials
every time you start andOTP.</string>
<string name="dialog_msg_security_default">By default the Android KeyStore will be used, however
this is known to cause problems on certain custom ROMs (and a few stock ones as well). You
can change the encryption in the Settings by clicking on the button below.</string>
<string name="dialog_msg_last_used">In order for andOTP to recognize which token was used last <string name="dialog_msg_last_used">In order for andOTP to recognize which token was used last
you have to have \"tap to reveal\" enabled or use the copy button.\n\nThis message will not you have to have \"tap to reveal\" enabled or use the copy button.\n\nThis message will not
be shown again.</string> be shown again.</string>
<string name="dialog_msg_keystore_error">Failed to load the encryption key from the KeyStore.
<b>Any entries that are added will get lost.</b>\n\nTo still be able to use andOTP you can go
to the Settings and switch the <b>Database encryption</b> to <b>Password / PIN</b>.</string>
</resources> </resources>

View file

@ -13,6 +13,7 @@
<string name="settings_title_auth">Authentication</string> <string name="settings_title_auth">Authentication</string>
<string name="settings_title_auth_password">Password</string> <string name="settings_title_auth_password">Password</string>
<string name="settings_title_auth_pin">PIN</string> <string name="settings_title_auth_pin">PIN</string>
<string name="settings_title_encryption">Database encryption</string>
<string name="settings_title_panic">Panic Trigger</string> <string name="settings_title_panic">Panic Trigger</string>
<string name="settings_title_lang">Language</string> <string name="settings_title_lang">Language</string>
@ -33,6 +34,7 @@
<string name="settings_title_special_features">Enable special features</string> <string name="settings_title_special_features">Enable special features</string>
<string name="settings_title_enable_screenshot">Enable screenshots</string> <string name="settings_title_enable_screenshot">Enable screenshots</string>
<string name="settings_title_enable_android_backup_service">Enable android backup</string> <string name="settings_title_enable_android_backup_service">Enable android backup</string>
<string name="settings_title_clear_keystore">Clear KeyStore</string>
<!-- Descriptions --> <!-- Descriptions -->
<string name="settings_desc_tap_to_reveal">Hide the OTP tokens by default, requiring them to be <string name="settings_desc_tap_to_reveal">Hide the OTP tokens by default, requiring them to be
@ -55,11 +57,13 @@
<string name="settings_desc_openpgp_verify">Encrypted backups are only imported if they are <string name="settings_desc_openpgp_verify">Encrypted backups are only imported if they are
signed with a valid key</string> signed with a valid key</string>
<string name="settings_desc_special_features">Uncheck to disable the special features again</string>
<string name="settings_desc_enable_screenshot">Allow to take screenshots of the main screen <string name="settings_desc_enable_screenshot">Allow to take screenshots of the main screen
(disabled by default for security reasons)</string> (disabled by default for security reasons)</string>
<string name="settings_desc_enable_android_backup_service">Enables andOTP to use android\'s <string name="settings_desc_enable_android_backup_service">Enables andOTP to use android\'s
built in backup service to bacup keys and preferences</string> built in backup service to bacup keys and preferences</string>
<string name="settings_desc_special_features">Uncheck to disable the special features again</string> <string name="settings_desc_special_features">Uncheck to disable the special features again</string>
<string name="settings_desc_clear_keystore">Delete the encryption key from the KeyStore</string>
<!-- Toasts --> <!-- Toasts -->
<string name="settings_toast_auth_device_pre_lollipop">This feature requires at least Android 5.0 <string name="settings_toast_auth_device_pre_lollipop">This feature requires at least Android 5.0
@ -67,6 +71,44 @@
<string name="settings_toast_auth_device_not_secure">This feature requires a secure lock screen <string name="settings_toast_auth_device_not_secure">This feature requires a secure lock screen
to be set up (Settings -> Security -> Screenlock)</string> to be set up (Settings -> Security -> Screenlock)</string>
<string name="settings_toast_password_empty">An empty password is not allowed, set the
Authentication to \"None\" to disable it!</string>
<string name="settings_toast_encryption_changing">Trying to change the database encryption,
please wait!</string>
<string name="settings_toast_encryption_change_success">Successfully changed the database
encryption!</string>
<string name="settings_toast_encryption_change_failed">Failed to change database encryption,
restored database from internal backup!</string>
<string name="settings_toast_encryption_backup_failed">Failed to create an internal
backup, aborting!</string>
<string name="settings_toast_encryption_no_key">Failed to get the encryption key, aborting!</string>
<string name="settings_toast_encryption_auth_failed">Authentication failed, aborting!</string>
<string name="settings_toast_auth_upgrade_failed">Failed to silently upgrade your password / PIN
to the new encryption, please manually reset it in the settings!</string>
<string name="settings_dialog_title_error">Error</string>
<string name="settings_dialog_title_clear_keystore">Clear the KeyStore?</string>
<string name="settings_dialog_msg_auth_invalid_with_encryption">You can only use Password or PIN as
long as the database encryption is set to \"Password / PIN\"!</string>
<string name="settings_dialog_msg_encryption_invalid_with_auth">You first need to set the
Authentication to \"Password\" or \"PIN\"!</string>
<string name="settings_dialog_msg_encryption_invalid_without_credentials">You first need to set a
Password or PIN before changing the encryption!</string>
<string name="settings_dialog_msg_clear_keystore_password">In some cases clearing the KeyStore
can help resolve problems. You should only proceed if you know what you are doing!\n\nSince
the <b>Database encryption</b> is set to <b>Password / PIN</b> you shouldn\'t lose any data
doing this (but it never hurts to have a backup anyways).\n\n<b>Are you really sure you want
to clear the KeyStore?</b></string>
<string name="settings_dialog_msg_clear_keystore_keystore">In some cases clearing the KeyStore
can help resolve problems. You should only proceed if you know what you are doing!\n\n<b>Warning</b>:
Since the <b>Database encryption</b> is set to <b>Android KeyStore</b> you will lose all
your accounts. Make sure you have a backup!\n\n<b>Are you really sure you want to clear the
KeyStore?</b></string>
<!-- List entries --> <!-- List entries -->
<string-array name="settings_entries_auth"> <string-array name="settings_entries_auth">
<item>None</item> <item>None</item>
@ -75,6 +117,11 @@
<item>Device credentials</item> <item>Device credentials</item>
</string-array> </string-array>
<string-array name="settings_entries_encryption">
<item>Android KeyStore</item>
<item>Password / PIN</item>
</string-array>
<string-array name="settings_entries_panic"> <string-array name="settings_entries_panic">
<item>Wipe all accounts</item> <item>Wipe all accounts</item>
<item>Reset app settings</item> <item>Reset app settings</item>
@ -97,9 +144,11 @@
<string name="settings_lang_sys_default">System default</string> <string name="settings_lang_sys_default">System default</string>
<!-- PasswordPreference --> <!-- PasswordPreference -->
<string name="settings_hint_password">Password</string> <string name="settings_hint_password">Enter new password</string>
<string name="settings_hint_pin">PIN</string> <string name="settings_hint_pin">Enter new PIN</string>
<string name="settings_hint_password_confirm">Confirm password</string> <string name="settings_hint_password_confirm">Confirm password</string>
<string name="settings_hint_pin_confirm">Confirm PIN</string> <string name="settings_hint_pin_confirm">Confirm PIN</string>
<string name="settings_hint_unchanged">(unchanged)</string>
<string name="settings_label_short_password">The password needs to be at least %1$d characters long!</string>
<string name="settings_label_short_pin">The PIN needs to be at least %1$d digits long!</string>
</resources> </resources>

View file

@ -9,14 +9,12 @@
<CheckBoxPreference <CheckBoxPreference
android:key="@string/settings_key_tap_to_reveal" android:key="@string/settings_key_tap_to_reveal"
android:order="1"
android:title="@string/settings_title_tap_to_reveal" android:title="@string/settings_title_tap_to_reveal"
android:summary="@string/settings_desc_tap_to_reveal" android:summary="@string/settings_desc_tap_to_reveal"
android:defaultValue="false" /> android:defaultValue="false" />
<com.vanniktech.vntnumberpickerpreference.VNTNumberPickerPreference <com.vanniktech.vntnumberpickerpreference.VNTNumberPickerPreference
android:key="@string/settings_key_tap_to_reveal_timeout" android:key="@string/settings_key_tap_to_reveal_timeout"
android:order="2"
android:title="@string/settings_title_tap_to_reveal_timeout" android:title="@string/settings_title_tap_to_reveal_timeout"
android:dialogMessage="@string/settings_desc_tap_to_reveal_timeout" android:dialogMessage="@string/settings_desc_tap_to_reveal_timeout"
android:defaultValue="@integer/settings_default_tap_to_reveal_timeout" android:defaultValue="@integer/settings_default_tap_to_reveal_timeout"
@ -24,18 +22,20 @@
app:vnt_minValue="@integer/settings_min_tap_to_reveal_timeout" app:vnt_minValue="@integer/settings_min_tap_to_reveal_timeout"
app:vnt_maxValue="@integer/settings_max_tap_to_reveal_timeout" /> app:vnt_maxValue="@integer/settings_max_tap_to_reveal_timeout" />
<ListPreference <org.shadowice.flocke.andotp.Preferences.CredentialsPreference
android:key="@string/settings_key_auth" android:key="@string/settings_key_auth"
android:order="3" android:title="@string/settings_title_auth" />
android:title="@string/settings_title_auth"
<ListPreference
android:key="@string/settings_key_encryption"
android:title="@string/settings_title_encryption"
android:summary="%s" android:summary="%s"
android:entries="@array/settings_entries_auth" android:entries="@array/settings_entries_encryption"
android:entryValues="@array/settings_values_auth" android:entryValues="@array/settings_values_encryption"
android:defaultValue="@string/settings_default_auth" /> android:defaultValue="@string/settings_default_encryption" />
<MultiSelectListPreference <MultiSelectListPreference
android:key="@string/settings_key_panic" android:key="@string/settings_key_panic"
android:order="5"
android:title="@string/settings_title_panic" android:title="@string/settings_title_panic"
android:summary="@string/settings_desc_panic" android:summary="@string/settings_desc_panic"
android:entries="@array/settings_entries_panic" android:entries="@array/settings_entries_panic"

View file

@ -23,6 +23,11 @@
android:summary="@string/settings_desc_enable_android_backup_service" android:summary="@string/settings_desc_enable_android_backup_service"
android:defaultValue="false" /> android:defaultValue="false" />
<Preference
android:key="@string/settings_key_clear_keystore"
android:title="@string/settings_title_clear_keystore"
android:summary="@string/settings_desc_clear_keystore" />
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>