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