mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-17 21:29:54 +01:00
Merge branch '1696-keystore-crash' into 'master'
Show a dialog instead of crashing if a hardware-backed key can't be loaded Closes #1696 See merge request briar/briar!1233
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
package org.briarproject.bramble.api.account;
|
package org.briarproject.bramble.api.account;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.crypto.DecryptionException;
|
||||||
import org.briarproject.bramble.api.crypto.SecretKey;
|
import org.briarproject.bramble.api.crypto.SecretKey;
|
||||||
import org.briarproject.bramble.api.identity.IdentityManager;
|
import org.briarproject.bramble.api.identity.IdentityManager;
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
@@ -52,17 +53,19 @@ public interface AccountManager {
|
|||||||
* Loads the encrypted database key from disk and decrypts it with the
|
* Loads the encrypted database key from disk and decrypts it with the
|
||||||
* given password.
|
* given password.
|
||||||
*
|
*
|
||||||
* @return true if the database key was successfully loaded and decrypted.
|
* @throws DecryptionException If the database key could not be loaded and
|
||||||
|
* decrypted.
|
||||||
*/
|
*/
|
||||||
boolean signIn(String password);
|
void signIn(String password) throws DecryptionException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the encrypted database key from disk, decrypts it with the old
|
* Loads the encrypted database key from disk, decrypts it with the old
|
||||||
* password, encrypts it with the new password, and stores it on disk,
|
* password, encrypts it with the new password, and stores it on disk,
|
||||||
* replacing the old key.
|
* replacing the old key.
|
||||||
*
|
*
|
||||||
* @return true if the database key was successfully loaded, re-encrypted
|
* @throws DecryptionException If the database key could not be loaded and
|
||||||
* and stored.
|
* decrypted.
|
||||||
*/
|
*/
|
||||||
boolean changePassword(String oldPassword, String newPassword);
|
void changePassword(String oldPassword, String newPassword)
|
||||||
|
throws DecryptionException;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,16 +142,17 @@ public interface CryptoComponent {
|
|||||||
/**
|
/**
|
||||||
* Decrypts and authenticates the given ciphertext that has been read from
|
* Decrypts and authenticates the given ciphertext that has been read from
|
||||||
* storage. The encryption and authentication keys are derived from the
|
* storage. The encryption and authentication keys are derived from the
|
||||||
* given password. Returns null if the ciphertext cannot be decrypted and
|
* given password.
|
||||||
* authenticated (for example, if the password is wrong).
|
|
||||||
*
|
*
|
||||||
* @param keyStrengthener Used to strengthen the password-based key. If
|
* @param keyStrengthener Used to strengthen the password-based key. If
|
||||||
* null, or if strengthening was not used when encrypting the ciphertext,
|
* null, or if strengthening was not used when encrypting the ciphertext,
|
||||||
* the password-based key will not be strengthened
|
* the password-based key will not be strengthened
|
||||||
|
* @throws DecryptionException If the ciphertext cannot be decrypted and
|
||||||
|
* authenticated (for example, if the password is wrong).
|
||||||
*/
|
*/
|
||||||
@Nullable
|
|
||||||
byte[] decryptWithPassword(byte[] ciphertext, String password,
|
byte[] decryptWithPassword(byte[] ciphertext, String password,
|
||||||
@Nullable KeyStrengthener keyStrengthener);
|
@Nullable KeyStrengthener keyStrengthener)
|
||||||
|
throws DecryptionException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the given ciphertext was encrypted using a strengthened
|
* Returns true if the given ciphertext was encrypted using a strengthened
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package org.briarproject.bramble.api.crypto;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
|
public class DecryptionException extends Exception {
|
||||||
|
|
||||||
|
private final DecryptionResult result;
|
||||||
|
|
||||||
|
public DecryptionException(DecryptionResult result) {
|
||||||
|
this.result = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DecryptionResult getDecryptionResult() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package org.briarproject.bramble.api.crypto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of a password-based decryption operation.
|
||||||
|
*/
|
||||||
|
public enum DecryptionResult {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decryption succeeded.
|
||||||
|
*/
|
||||||
|
SUCCESS,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decryption failed because the format of the ciphertext was invalid.
|
||||||
|
*/
|
||||||
|
INVALID_CIPHERTEXT,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decryption failed because the {@link KeyStrengthener} used for
|
||||||
|
* encryption was not available for decryption.
|
||||||
|
*/
|
||||||
|
KEY_STRENGTHENER_ERROR,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decryption failed because the password used for decryption did not match
|
||||||
|
* the password used for encryption.
|
||||||
|
*/
|
||||||
|
INVALID_PASSWORD
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package org.briarproject.bramble.account;
|
|||||||
|
|
||||||
import org.briarproject.bramble.api.account.AccountManager;
|
import org.briarproject.bramble.api.account.AccountManager;
|
||||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||||
|
import org.briarproject.bramble.api.crypto.DecryptionException;
|
||||||
import org.briarproject.bramble.api.crypto.KeyStrengthener;
|
import org.briarproject.bramble.api.crypto.KeyStrengthener;
|
||||||
import org.briarproject.bramble.api.crypto.SecretKey;
|
import org.briarproject.bramble.api.crypto.SecretKey;
|
||||||
import org.briarproject.bramble.api.db.DatabaseConfig;
|
import org.briarproject.bramble.api.db.DatabaseConfig;
|
||||||
@@ -17,6 +18,7 @@ import java.io.FileInputStream;
|
|||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
@@ -24,6 +26,7 @@ import javax.annotation.concurrent.GuardedBy;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
|
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_CIPHERTEXT;
|
||||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
import static org.briarproject.bramble.util.StringUtils.fromHexString;
|
import static org.briarproject.bramble.util.StringUtils.fromHexString;
|
||||||
import static org.briarproject.bramble.util.StringUtils.toHexString;
|
import static org.briarproject.bramble.util.StringUtils.toHexString;
|
||||||
@@ -95,7 +98,7 @@ class AccountManagerImpl implements AccountManager {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(
|
BufferedReader reader = new BufferedReader(new InputStreamReader(
|
||||||
new FileInputStream(f), "UTF-8"));
|
new FileInputStream(f), Charset.forName("UTF-8")));
|
||||||
String key = reader.readLine();
|
String key = reader.readLine();
|
||||||
reader.close();
|
reader.close();
|
||||||
return key;
|
return key;
|
||||||
@@ -147,7 +150,7 @@ class AccountManagerImpl implements AccountManager {
|
|||||||
@GuardedBy("stateChangeLock")
|
@GuardedBy("stateChangeLock")
|
||||||
private void writeDbKeyToFile(String key, File f) throws IOException {
|
private void writeDbKeyToFile(String key, File f) throws IOException {
|
||||||
FileOutputStream out = new FileOutputStream(f);
|
FileOutputStream out = new FileOutputStream(f);
|
||||||
out.write(key.getBytes("UTF-8"));
|
out.write(key.getBytes(Charset.forName("UTF-8")));
|
||||||
out.flush();
|
out.flush();
|
||||||
out.close();
|
out.close();
|
||||||
}
|
}
|
||||||
@@ -192,31 +195,24 @@ class AccountManagerImpl implements AccountManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean signIn(String password) {
|
public void signIn(String password) throws DecryptionException {
|
||||||
synchronized (stateChangeLock) {
|
synchronized (stateChangeLock) {
|
||||||
SecretKey key = loadAndDecryptDatabaseKey(password);
|
databaseKey = loadAndDecryptDatabaseKey(password);
|
||||||
if (key == null) return false;
|
|
||||||
databaseKey = key;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@GuardedBy("stateChangeLock")
|
@GuardedBy("stateChangeLock")
|
||||||
@Nullable
|
private SecretKey loadAndDecryptDatabaseKey(String password)
|
||||||
private SecretKey loadAndDecryptDatabaseKey(String password) {
|
throws DecryptionException {
|
||||||
String hex = loadEncryptedDatabaseKey();
|
String hex = loadEncryptedDatabaseKey();
|
||||||
if (hex == null) {
|
if (hex == null) {
|
||||||
LOG.warning("Failed to load encrypted database key");
|
LOG.warning("Failed to load encrypted database key");
|
||||||
return null;
|
throw new DecryptionException(INVALID_CIPHERTEXT);
|
||||||
}
|
}
|
||||||
byte[] ciphertext = fromHexString(hex);
|
byte[] ciphertext = fromHexString(hex);
|
||||||
KeyStrengthener keyStrengthener = databaseConfig.getKeyStrengthener();
|
KeyStrengthener keyStrengthener = databaseConfig.getKeyStrengthener();
|
||||||
byte[] plaintext = crypto.decryptWithPassword(ciphertext, password,
|
byte[] plaintext = crypto.decryptWithPassword(ciphertext, password,
|
||||||
keyStrengthener);
|
keyStrengthener);
|
||||||
if (plaintext == null) {
|
|
||||||
LOG.info("Failed to decrypt database key");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
SecretKey key = new SecretKey(plaintext);
|
SecretKey key = new SecretKey(plaintext);
|
||||||
// If the DB key was encrypted with a weak key and a key strengthener
|
// If the DB key was encrypted with a weak key and a key strengthener
|
||||||
// is now available, re-encrypt the DB key with a strengthened key
|
// is now available, re-encrypt the DB key with a strengthened key
|
||||||
@@ -229,10 +225,11 @@ class AccountManagerImpl implements AccountManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean changePassword(String oldPassword, String newPassword) {
|
public void changePassword(String oldPassword, String newPassword)
|
||||||
|
throws DecryptionException {
|
||||||
synchronized (stateChangeLock) {
|
synchronized (stateChangeLock) {
|
||||||
SecretKey key = loadAndDecryptDatabaseKey(oldPassword);
|
SecretKey key = loadAndDecryptDatabaseKey(oldPassword);
|
||||||
return key != null && encryptAndStoreDatabaseKey(key, newPassword);
|
encryptAndStoreDatabaseKey(key, newPassword);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import net.i2p.crypto.eddsa.KeyPairGenerator;
|
|||||||
import org.briarproject.bramble.api.crypto.AgreementPrivateKey;
|
import org.briarproject.bramble.api.crypto.AgreementPrivateKey;
|
||||||
import org.briarproject.bramble.api.crypto.AgreementPublicKey;
|
import org.briarproject.bramble.api.crypto.AgreementPublicKey;
|
||||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||||
|
import org.briarproject.bramble.api.crypto.DecryptionException;
|
||||||
import org.briarproject.bramble.api.crypto.KeyPair;
|
import org.briarproject.bramble.api.crypto.KeyPair;
|
||||||
import org.briarproject.bramble.api.crypto.KeyParser;
|
import org.briarproject.bramble.api.crypto.KeyParser;
|
||||||
import org.briarproject.bramble.api.crypto.KeyStrengthener;
|
import org.briarproject.bramble.api.crypto.KeyStrengthener;
|
||||||
@@ -39,6 +40,9 @@ import static java.lang.System.arraycopy;
|
|||||||
import static java.util.logging.Level.INFO;
|
import static java.util.logging.Level.INFO;
|
||||||
import static org.briarproject.bramble.api.crypto.CryptoConstants.KEY_TYPE_AGREEMENT;
|
import static org.briarproject.bramble.api.crypto.CryptoConstants.KEY_TYPE_AGREEMENT;
|
||||||
import static org.briarproject.bramble.api.crypto.CryptoConstants.KEY_TYPE_SIGNATURE;
|
import static org.briarproject.bramble.api.crypto.CryptoConstants.KEY_TYPE_SIGNATURE;
|
||||||
|
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_CIPHERTEXT;
|
||||||
|
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_PASSWORD;
|
||||||
|
import static org.briarproject.bramble.api.crypto.DecryptionResult.KEY_STRENGTHENER_ERROR;
|
||||||
import static org.briarproject.bramble.util.ByteUtils.INT_32_BYTES;
|
import static org.briarproject.bramble.util.ByteUtils.INT_32_BYTES;
|
||||||
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
||||||
import static org.briarproject.bramble.util.LogUtils.now;
|
import static org.briarproject.bramble.util.LogUtils.now;
|
||||||
@@ -359,16 +363,17 @@ class CryptoComponentImpl implements CryptoComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
|
||||||
public byte[] decryptWithPassword(byte[] input, String password,
|
public byte[] decryptWithPassword(byte[] input, String password,
|
||||||
@Nullable KeyStrengthener keyStrengthener) {
|
@Nullable KeyStrengthener keyStrengthener)
|
||||||
|
throws DecryptionException {
|
||||||
AuthenticatedCipher cipher = new XSalsa20Poly1305AuthenticatedCipher();
|
AuthenticatedCipher cipher = new XSalsa20Poly1305AuthenticatedCipher();
|
||||||
int macBytes = cipher.getMacBytes();
|
int macBytes = cipher.getMacBytes();
|
||||||
// The input contains the format version, salt, cost parameter, IV,
|
// The input contains the format version, salt, cost parameter, IV,
|
||||||
// ciphertext and MAC
|
// ciphertext and MAC
|
||||||
if (input.length < 1 + PBKDF_SALT_BYTES + INT_32_BYTES
|
if (input.length < 1 + PBKDF_SALT_BYTES + INT_32_BYTES
|
||||||
+ STORAGE_IV_BYTES + macBytes)
|
+ STORAGE_IV_BYTES + macBytes) {
|
||||||
return null; // Invalid input
|
throw new DecryptionException(INVALID_CIPHERTEXT);
|
||||||
|
}
|
||||||
int inputOff = 0;
|
int inputOff = 0;
|
||||||
// Format version
|
// Format version
|
||||||
byte formatVersion = input[inputOff];
|
byte formatVersion = input[inputOff];
|
||||||
@@ -376,7 +381,7 @@ class CryptoComponentImpl implements CryptoComponent {
|
|||||||
// Check whether we support this format version
|
// Check whether we support this format version
|
||||||
if (formatVersion != PBKDF_FORMAT_SCRYPT &&
|
if (formatVersion != PBKDF_FORMAT_SCRYPT &&
|
||||||
formatVersion != PBKDF_FORMAT_SCRYPT_STRENGTHENED) {
|
formatVersion != PBKDF_FORMAT_SCRYPT_STRENGTHENED) {
|
||||||
return null;
|
throw new DecryptionException(INVALID_CIPHERTEXT);
|
||||||
}
|
}
|
||||||
// Salt
|
// Salt
|
||||||
byte[] salt = new byte[PBKDF_SALT_BYTES];
|
byte[] salt = new byte[PBKDF_SALT_BYTES];
|
||||||
@@ -385,8 +390,9 @@ class CryptoComponentImpl implements CryptoComponent {
|
|||||||
// Cost parameter
|
// Cost parameter
|
||||||
long cost = ByteUtils.readUint32(input, inputOff);
|
long cost = ByteUtils.readUint32(input, inputOff);
|
||||||
inputOff += INT_32_BYTES;
|
inputOff += INT_32_BYTES;
|
||||||
if (cost < 2 || cost > Integer.MAX_VALUE)
|
if (cost < 2 || cost > Integer.MAX_VALUE) {
|
||||||
return null; // Invalid cost parameter
|
throw new DecryptionException(INVALID_CIPHERTEXT);
|
||||||
|
}
|
||||||
// IV
|
// IV
|
||||||
byte[] iv = new byte[STORAGE_IV_BYTES];
|
byte[] iv = new byte[STORAGE_IV_BYTES];
|
||||||
arraycopy(input, inputOff, iv, 0, iv.length);
|
arraycopy(input, inputOff, iv, 0, iv.length);
|
||||||
@@ -394,8 +400,10 @@ class CryptoComponentImpl implements CryptoComponent {
|
|||||||
// Derive the decryption key from the password
|
// Derive the decryption key from the password
|
||||||
SecretKey key = passwordBasedKdf.deriveKey(password, salt, (int) cost);
|
SecretKey key = passwordBasedKdf.deriveKey(password, salt, (int) cost);
|
||||||
if (formatVersion == PBKDF_FORMAT_SCRYPT_STRENGTHENED) {
|
if (formatVersion == PBKDF_FORMAT_SCRYPT_STRENGTHENED) {
|
||||||
if (keyStrengthener == null || !keyStrengthener.isInitialised())
|
if (keyStrengthener == null || !keyStrengthener.isInitialised()) {
|
||||||
return null; // Can't derive the same strengthened key
|
// Can't derive the same strengthened key
|
||||||
|
throw new DecryptionException(KEY_STRENGTHENER_ERROR);
|
||||||
|
}
|
||||||
key = keyStrengthener.strengthenKey(key);
|
key = keyStrengthener.strengthenKey(key);
|
||||||
}
|
}
|
||||||
// Initialise the cipher
|
// Initialise the cipher
|
||||||
@@ -411,7 +419,7 @@ class CryptoComponentImpl implements CryptoComponent {
|
|||||||
cipher.process(input, inputOff, inputLen, output, 0);
|
cipher.process(input, inputOff, inputLen, output, 0);
|
||||||
return output;
|
return output;
|
||||||
} catch (GeneralSecurityException e) {
|
} catch (GeneralSecurityException e) {
|
||||||
return null; // Invalid ciphertext
|
throw new DecryptionException(INVALID_PASSWORD);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.briarproject.bramble.account;
|
package org.briarproject.bramble.account;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||||
|
import org.briarproject.bramble.api.crypto.DecryptionException;
|
||||||
import org.briarproject.bramble.api.crypto.KeyStrengthener;
|
import org.briarproject.bramble.api.crypto.KeyStrengthener;
|
||||||
import org.briarproject.bramble.api.crypto.SecretKey;
|
import org.briarproject.bramble.api.crypto.SecretKey;
|
||||||
import org.briarproject.bramble.api.db.DatabaseConfig;
|
import org.briarproject.bramble.api.db.DatabaseConfig;
|
||||||
@@ -19,12 +20,15 @@ import java.io.FileInputStream;
|
|||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
import static junit.framework.Assert.assertFalse;
|
import static junit.framework.Assert.assertFalse;
|
||||||
import static junit.framework.Assert.assertNull;
|
import static junit.framework.Assert.assertNull;
|
||||||
import static junit.framework.Assert.assertTrue;
|
import static junit.framework.Assert.assertTrue;
|
||||||
|
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_CIPHERTEXT;
|
||||||
|
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_PASSWORD;
|
||||||
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
|
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
|
||||||
import static org.briarproject.bramble.test.TestUtils.getIdentity;
|
import static org.briarproject.bramble.test.TestUtils.getIdentity;
|
||||||
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
|
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
|
||||||
@@ -35,6 +39,7 @@ import static org.briarproject.bramble.util.StringUtils.toHexString;
|
|||||||
import static org.junit.Assert.assertArrayEquals;
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
public class AccountManagerImplTest extends BrambleMockTestCase {
|
public class AccountManagerImplTest extends BrambleMockTestCase {
|
||||||
|
|
||||||
@@ -83,8 +88,13 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSignInReturnsFalseIfDbKeyCannotBeLoaded() {
|
public void testSignInThrowsExceptionIfDbKeyCannotBeLoaded() {
|
||||||
assertFalse(accountManager.signIn(password));
|
try {
|
||||||
|
accountManager.signIn(password);
|
||||||
|
fail();
|
||||||
|
} catch (DecryptionException expected) {
|
||||||
|
assertEquals(INVALID_CIPHERTEXT, expected.getDecryptionResult());
|
||||||
|
}
|
||||||
assertFalse(accountManager.hasDatabaseKey());
|
assertFalse(accountManager.hasDatabaseKey());
|
||||||
|
|
||||||
assertFalse(keyFile.exists());
|
assertFalse(keyFile.exists());
|
||||||
@@ -92,11 +102,11 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSignInReturnsFalseIfPasswordIsWrong() throws Exception {
|
public void testSignInThrowsExceptionIfPasswordIsWrong() throws Exception {
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
oneOf(crypto).decryptWithPassword(encryptedKey, password,
|
oneOf(crypto).decryptWithPassword(encryptedKey, password,
|
||||||
keyStrengthener);
|
keyStrengthener);
|
||||||
will(returnValue(null));
|
will(throwException(new DecryptionException(INVALID_PASSWORD)));
|
||||||
}});
|
}});
|
||||||
|
|
||||||
storeDatabaseKey(keyFile, encryptedKeyHex);
|
storeDatabaseKey(keyFile, encryptedKeyHex);
|
||||||
@@ -105,7 +115,12 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
|
|||||||
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
|
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
|
||||||
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
|
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
|
||||||
|
|
||||||
assertFalse(accountManager.signIn(password));
|
try {
|
||||||
|
accountManager.signIn(password);
|
||||||
|
fail();
|
||||||
|
} catch (DecryptionException expected) {
|
||||||
|
assertEquals(INVALID_PASSWORD, expected.getDecryptionResult());
|
||||||
|
}
|
||||||
assertFalse(accountManager.hasDatabaseKey());
|
assertFalse(accountManager.hasDatabaseKey());
|
||||||
|
|
||||||
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
|
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
|
||||||
@@ -128,7 +143,7 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
|
|||||||
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
|
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
|
||||||
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
|
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
|
||||||
|
|
||||||
assertTrue(accountManager.signIn(password));
|
accountManager.signIn(password);
|
||||||
assertTrue(accountManager.hasDatabaseKey());
|
assertTrue(accountManager.hasDatabaseKey());
|
||||||
SecretKey decrypted = accountManager.getDatabaseKey();
|
SecretKey decrypted = accountManager.getDatabaseKey();
|
||||||
assertNotNull(decrypted);
|
assertNotNull(decrypted);
|
||||||
@@ -157,7 +172,7 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
|
|||||||
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
|
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
|
||||||
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
|
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
|
||||||
|
|
||||||
assertTrue(accountManager.signIn(password));
|
accountManager.signIn(password);
|
||||||
assertTrue(accountManager.hasDatabaseKey());
|
assertTrue(accountManager.hasDatabaseKey());
|
||||||
SecretKey decrypted = accountManager.getDatabaseKey();
|
SecretKey decrypted = accountManager.getDatabaseKey();
|
||||||
assertNotNull(decrypted);
|
assertNotNull(decrypted);
|
||||||
@@ -266,26 +281,36 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testChangePasswordReturnsFalseIfDbKeyCannotBeLoaded() {
|
public void testChangePasswordThrowsExceptionIfDbKeyCannotBeLoaded() {
|
||||||
assertFalse(accountManager.changePassword(password, newPassword));
|
try {
|
||||||
|
accountManager.changePassword(password, newPassword);
|
||||||
|
fail();
|
||||||
|
} catch (DecryptionException expected) {
|
||||||
|
assertEquals(INVALID_CIPHERTEXT, expected.getDecryptionResult());
|
||||||
|
}
|
||||||
|
|
||||||
assertFalse(keyFile.exists());
|
assertFalse(keyFile.exists());
|
||||||
assertFalse(keyBackupFile.exists());
|
assertFalse(keyBackupFile.exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testChangePasswordReturnsFalseIfPasswordIsWrong()
|
public void testChangePasswordThrowsExceptionIfPasswordIsWrong()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
oneOf(crypto).decryptWithPassword(encryptedKey, password,
|
oneOf(crypto).decryptWithPassword(encryptedKey, password,
|
||||||
keyStrengthener);
|
keyStrengthener);
|
||||||
will(returnValue(null));
|
will(throwException(new DecryptionException(INVALID_PASSWORD)));
|
||||||
}});
|
}});
|
||||||
|
|
||||||
storeDatabaseKey(keyFile, encryptedKeyHex);
|
storeDatabaseKey(keyFile, encryptedKeyHex);
|
||||||
storeDatabaseKey(keyBackupFile, encryptedKeyHex);
|
storeDatabaseKey(keyBackupFile, encryptedKeyHex);
|
||||||
|
|
||||||
assertFalse(accountManager.changePassword(password, newPassword));
|
try {
|
||||||
|
accountManager.changePassword(password, newPassword);
|
||||||
|
fail();
|
||||||
|
} catch (DecryptionException expected) {
|
||||||
|
assertEquals(INVALID_PASSWORD, expected.getDecryptionResult());
|
||||||
|
}
|
||||||
|
|
||||||
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
|
assertEquals(encryptedKeyHex, loadDatabaseKey(keyFile));
|
||||||
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
|
assertEquals(encryptedKeyHex, loadDatabaseKey(keyBackupFile));
|
||||||
@@ -308,7 +333,7 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
|
|||||||
storeDatabaseKey(keyFile, encryptedKeyHex);
|
storeDatabaseKey(keyFile, encryptedKeyHex);
|
||||||
storeDatabaseKey(keyBackupFile, encryptedKeyHex);
|
storeDatabaseKey(keyBackupFile, encryptedKeyHex);
|
||||||
|
|
||||||
assertTrue(accountManager.changePassword(password, newPassword));
|
accountManager.changePassword(password, newPassword);
|
||||||
|
|
||||||
assertEquals(newEncryptedKeyHex, loadDatabaseKey(keyFile));
|
assertEquals(newEncryptedKeyHex, loadDatabaseKey(keyFile));
|
||||||
assertEquals(newEncryptedKeyHex, loadDatabaseKey(keyBackupFile));
|
assertEquals(newEncryptedKeyHex, loadDatabaseKey(keyBackupFile));
|
||||||
@@ -317,7 +342,7 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
|
|||||||
private void storeDatabaseKey(File f, String hex) throws IOException {
|
private void storeDatabaseKey(File f, String hex) throws IOException {
|
||||||
f.getParentFile().mkdirs();
|
f.getParentFile().mkdirs();
|
||||||
FileOutputStream out = new FileOutputStream(f);
|
FileOutputStream out = new FileOutputStream(f);
|
||||||
out.write(hex.getBytes("UTF-8"));
|
out.write(hex.getBytes(Charset.forName("UTF-8")));
|
||||||
out.flush();
|
out.flush();
|
||||||
out.close();
|
out.close();
|
||||||
}
|
}
|
||||||
@@ -325,7 +350,7 @@ public class AccountManagerImplTest extends BrambleMockTestCase {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private String loadDatabaseKey(File f) throws IOException {
|
private String loadDatabaseKey(File f) throws IOException {
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(
|
BufferedReader reader = new BufferedReader(new InputStreamReader(
|
||||||
new FileInputStream(f), "UTF-8"));
|
new FileInputStream(f), Charset.forName("UTF-8")));
|
||||||
String hex = reader.readLine();
|
String hex = reader.readLine();
|
||||||
reader.close();
|
reader.close();
|
||||||
return hex;
|
return hex;
|
||||||
|
|||||||
@@ -1,25 +1,35 @@
|
|||||||
package org.briarproject.bramble.crypto;
|
package org.briarproject.bramble.crypto;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.crypto.DecryptionException;
|
||||||
|
import org.briarproject.bramble.api.crypto.KeyStrengthener;
|
||||||
|
import org.briarproject.bramble.api.crypto.SecretKey;
|
||||||
import org.briarproject.bramble.system.SystemClock;
|
import org.briarproject.bramble.system.SystemClock;
|
||||||
import org.briarproject.bramble.test.BrambleTestCase;
|
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||||
import org.briarproject.bramble.test.TestSecureRandomProvider;
|
import org.briarproject.bramble.test.TestSecureRandomProvider;
|
||||||
import org.briarproject.bramble.test.TestUtils;
|
import org.jmock.Expectations;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.Random;
|
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_CIPHERTEXT;
|
||||||
|
import static org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_PASSWORD;
|
||||||
|
import static org.briarproject.bramble.api.crypto.DecryptionResult.KEY_STRENGTHENER_ERROR;
|
||||||
|
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
|
||||||
|
import static org.briarproject.bramble.test.TestUtils.getSecretKey;
|
||||||
import static org.junit.Assert.assertArrayEquals;
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
public class PasswordBasedEncryptionTest extends BrambleTestCase {
|
public class PasswordBasedEncryptionTest extends BrambleMockTestCase {
|
||||||
|
|
||||||
|
private final KeyStrengthener keyStrengthener =
|
||||||
|
context.mock(KeyStrengthener.class);
|
||||||
|
|
||||||
private final CryptoComponentImpl crypto =
|
private final CryptoComponentImpl crypto =
|
||||||
new CryptoComponentImpl(new TestSecureRandomProvider(),
|
new CryptoComponentImpl(new TestSecureRandomProvider(),
|
||||||
new ScryptKdf(new SystemClock()));
|
new ScryptKdf(new SystemClock()));
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testEncryptionAndDecryption() {
|
public void testEncryptionAndDecryption() throws Exception {
|
||||||
byte[] input = TestUtils.getRandomBytes(1234);
|
byte[] input = getRandomBytes(1234);
|
||||||
String password = "password";
|
String password = "password";
|
||||||
byte[] ciphertext = crypto.encryptWithPassword(input, password, null);
|
byte[] ciphertext = crypto.encryptWithPassword(input, password, null);
|
||||||
byte[] output = crypto.decryptWithPassword(ciphertext, password, null);
|
byte[] output = crypto.decryptWithPassword(ciphertext, password, null);
|
||||||
@@ -27,14 +37,80 @@ public class PasswordBasedEncryptionTest extends BrambleTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testInvalidCiphertextReturnsNull() {
|
public void testInvalidFormatVersionThrowsException() {
|
||||||
byte[] input = TestUtils.getRandomBytes(1234);
|
byte[] input = getRandomBytes(1234);
|
||||||
String password = "password";
|
String password = "password";
|
||||||
byte[] ciphertext = crypto.encryptWithPassword(input, password, null);
|
byte[] ciphertext = crypto.encryptWithPassword(input, password, null);
|
||||||
// Modify the ciphertext
|
|
||||||
int position = new Random().nextInt(ciphertext.length);
|
// Modify the format version
|
||||||
ciphertext[position] = (byte) (ciphertext[position] ^ 0xFF);
|
ciphertext[0] ^= (byte) 0xFF;
|
||||||
byte[] output = crypto.decryptWithPassword(ciphertext, password, null);
|
try {
|
||||||
assertNull(output);
|
crypto.decryptWithPassword(ciphertext, password, null);
|
||||||
|
fail();
|
||||||
|
} catch (DecryptionException expected) {
|
||||||
|
assertEquals(INVALID_CIPHERTEXT, expected.getDecryptionResult());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidPasswordThrowsException() {
|
||||||
|
byte[] input = getRandomBytes(1234);
|
||||||
|
byte[] ciphertext = crypto.encryptWithPassword(input, "password", null);
|
||||||
|
|
||||||
|
// Try to decrypt with the wrong password
|
||||||
|
try {
|
||||||
|
crypto.decryptWithPassword(ciphertext, "wrong", null);
|
||||||
|
fail();
|
||||||
|
} catch (DecryptionException expected) {
|
||||||
|
assertEquals(INVALID_PASSWORD, expected.getDecryptionResult());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMissingKeyStrengthenerThrowsException() {
|
||||||
|
SecretKey strengthened = getSecretKey();
|
||||||
|
context.checking(new Expectations() {{
|
||||||
|
oneOf(keyStrengthener).strengthenKey(with(any(SecretKey.class)));
|
||||||
|
will(returnValue(strengthened));
|
||||||
|
}});
|
||||||
|
|
||||||
|
// Use the key strengthener during encryption
|
||||||
|
byte[] input = getRandomBytes(1234);
|
||||||
|
String password = "password";
|
||||||
|
byte[] ciphertext =
|
||||||
|
crypto.encryptWithPassword(input, password, keyStrengthener);
|
||||||
|
|
||||||
|
// The key strengthener is missing during decryption
|
||||||
|
try {
|
||||||
|
crypto.decryptWithPassword(ciphertext, password, null);
|
||||||
|
fail();
|
||||||
|
} catch (DecryptionException expected) {
|
||||||
|
assertEquals(KEY_STRENGTHENER_ERROR, expected.getDecryptionResult());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testKeyStrengthenerFailureThrowsException() {
|
||||||
|
SecretKey strengthened = getSecretKey();
|
||||||
|
context.checking(new Expectations() {{
|
||||||
|
oneOf(keyStrengthener).strengthenKey(with(any(SecretKey.class)));
|
||||||
|
will(returnValue(strengthened));
|
||||||
|
oneOf(keyStrengthener).isInitialised();
|
||||||
|
will(returnValue(false));
|
||||||
|
}});
|
||||||
|
|
||||||
|
// Use the key strengthener during encryption
|
||||||
|
byte[] input = getRandomBytes(1234);
|
||||||
|
String password = "password";
|
||||||
|
byte[] ciphertext =
|
||||||
|
crypto.encryptWithPassword(input, password, keyStrengthener);
|
||||||
|
|
||||||
|
// The key strengthener fails during decryption
|
||||||
|
try {
|
||||||
|
crypto.decryptWithPassword(ciphertext, password, keyStrengthener);
|
||||||
|
fail();
|
||||||
|
} catch (DecryptionException expected) {
|
||||||
|
assertEquals(KEY_STRENGTHENER_ERROR, expected.getDecryptionResult());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ import static android.security.keystore.KeyProperties.PURPOSE_SIGN;
|
|||||||
import static java.util.Arrays.asList;
|
import static java.util.Arrays.asList;
|
||||||
import static java.util.Collections.singletonList;
|
import static java.util.Collections.singletonList;
|
||||||
import static java.util.logging.Level.INFO;
|
import static java.util.logging.Level.INFO;
|
||||||
|
import static java.util.logging.Level.WARNING;
|
||||||
import static java.util.logging.Logger.getLogger;
|
import static java.util.logging.Logger.getLogger;
|
||||||
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
|
|
||||||
@RequiresApi(23)
|
@RequiresApi(23)
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
@@ -79,7 +81,10 @@ class AndroidKeyStrengthener implements KeyStrengthener {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
} catch (GeneralSecurityException | IOException e) {
|
} catch (GeneralSecurityException e) {
|
||||||
|
logException(LOG, WARNING, e);
|
||||||
|
return false;
|
||||||
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import org.briarproject.bramble.util.AndroidUtils;
|
|||||||
import org.briarproject.bramble.util.StringUtils;
|
import org.briarproject.bramble.util.StringUtils;
|
||||||
import org.briarproject.briar.android.account.LockManagerImpl;
|
import org.briarproject.briar.android.account.LockManagerImpl;
|
||||||
import org.briarproject.briar.android.keyagreement.ContactExchangeModule;
|
import org.briarproject.briar.android.keyagreement.ContactExchangeModule;
|
||||||
|
import org.briarproject.briar.android.login.LoginModule;
|
||||||
import org.briarproject.briar.android.viewmodel.ViewModelModule;
|
import org.briarproject.briar.android.viewmodel.ViewModelModule;
|
||||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||||
import org.briarproject.briar.api.android.DozeWatchdog;
|
import org.briarproject.briar.api.android.DozeWatchdog;
|
||||||
@@ -64,7 +65,11 @@ import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_ONIO
|
|||||||
import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_PUBLIC_KEY_HEX;
|
import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_PUBLIC_KEY_HEX;
|
||||||
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
|
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
|
||||||
|
|
||||||
@Module(includes = {ContactExchangeModule.class, ViewModelModule.class})
|
@Module(includes = {
|
||||||
|
ContactExchangeModule.class,
|
||||||
|
LoginModule.class,
|
||||||
|
ViewModelModule.class
|
||||||
|
})
|
||||||
public class AppModule {
|
public class AppModule {
|
||||||
|
|
||||||
static class EagerSingletons {
|
static class EagerSingletons {
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import org.briarproject.briar.android.controller.BriarController;
|
|||||||
import org.briarproject.briar.android.controller.BriarControllerImpl;
|
import org.briarproject.briar.android.controller.BriarControllerImpl;
|
||||||
import org.briarproject.briar.android.controller.DbController;
|
import org.briarproject.briar.android.controller.DbController;
|
||||||
import org.briarproject.briar.android.controller.DbControllerImpl;
|
import org.briarproject.briar.android.controller.DbControllerImpl;
|
||||||
import org.briarproject.briar.android.login.ChangePasswordController;
|
|
||||||
import org.briarproject.briar.android.login.ChangePasswordControllerImpl;
|
|
||||||
import org.briarproject.briar.android.navdrawer.NavDrawerController;
|
import org.briarproject.briar.android.navdrawer.NavDrawerController;
|
||||||
import org.briarproject.briar.android.navdrawer.NavDrawerControllerImpl;
|
import org.briarproject.briar.android.navdrawer.NavDrawerControllerImpl;
|
||||||
|
|
||||||
@@ -46,13 +44,6 @@ public class ActivityModule {
|
|||||||
return setupController;
|
return setupController;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ActivityScope
|
|
||||||
@Provides
|
|
||||||
ChangePasswordController providePasswordController(
|
|
||||||
ChangePasswordControllerImpl passwordController) {
|
|
||||||
return passwordController;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ActivityScope
|
@ActivityScope
|
||||||
@Provides
|
@Provides
|
||||||
protected BriarController provideBriarController(
|
protected BriarController provideBriarController(
|
||||||
@@ -80,5 +71,4 @@ public class ActivityModule {
|
|||||||
BriarServiceConnection provideBriarServiceConnection() {
|
BriarServiceConnection provideBriarServiceConnection() {
|
||||||
return new BriarServiceConnection();
|
return new BriarServiceConnection();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,27 +15,33 @@ import android.widget.Toast;
|
|||||||
|
|
||||||
import com.google.android.material.textfield.TextInputLayout;
|
import com.google.android.material.textfield.TextInputLayout;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.crypto.DecryptionResult;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||||
import org.briarproject.briar.android.activity.BriarActivity;
|
import org.briarproject.briar.android.activity.BriarActivity;
|
||||||
import org.briarproject.briar.android.controller.handler.UiResultHandler;
|
|
||||||
import org.briarproject.briar.android.util.UiUtils;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
|
|
||||||
import static android.view.View.INVISIBLE;
|
import static android.view.View.INVISIBLE;
|
||||||
import static android.view.View.VISIBLE;
|
import static android.view.View.VISIBLE;
|
||||||
|
import static android.widget.Toast.LENGTH_LONG;
|
||||||
|
import static org.briarproject.bramble.api.crypto.DecryptionResult.KEY_STRENGTHENER_ERROR;
|
||||||
|
import static org.briarproject.bramble.api.crypto.DecryptionResult.SUCCESS;
|
||||||
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK;
|
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK;
|
||||||
|
import static org.briarproject.briar.android.login.LoginUtils.createKeyStrengthenerErrorDialog;
|
||||||
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
|
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
|
||||||
|
import static org.briarproject.briar.android.util.UiUtils.setError;
|
||||||
import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard;
|
import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard;
|
||||||
|
|
||||||
public class ChangePasswordActivity extends BriarActivity
|
public class ChangePasswordActivity extends BriarActivity
|
||||||
implements OnClickListener, OnEditorActionListener {
|
implements OnClickListener, OnEditorActionListener {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
protected ChangePasswordController passwordController;
|
ViewModelProvider.Factory viewModelFactory;
|
||||||
|
|
||||||
private TextInputLayout currentPasswordEntryWrapper;
|
private TextInputLayout currentPasswordEntryWrapper;
|
||||||
private TextInputLayout newPasswordEntryWrapper;
|
private TextInputLayout newPasswordEntryWrapper;
|
||||||
@@ -47,11 +53,17 @@ public class ChangePasswordActivity extends BriarActivity
|
|||||||
private Button changePasswordButton;
|
private Button changePasswordButton;
|
||||||
private ProgressBar progress;
|
private ProgressBar progress;
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
ChangePasswordViewModel viewModel;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle state) {
|
public void onCreate(Bundle state) {
|
||||||
super.onCreate(state);
|
super.onCreate(state);
|
||||||
setContentView(R.layout.activity_change_password);
|
setContentView(R.layout.activity_change_password);
|
||||||
|
|
||||||
|
viewModel = ViewModelProviders.of(this, viewModelFactory)
|
||||||
|
.get(ChangePasswordViewModel.class);
|
||||||
|
|
||||||
currentPasswordEntryWrapper =
|
currentPasswordEntryWrapper =
|
||||||
findViewById(R.id.current_password_entry_wrapper);
|
findViewById(R.id.current_password_entry_wrapper);
|
||||||
newPasswordEntryWrapper = findViewById(R.id.new_password_entry_wrapper);
|
newPasswordEntryWrapper = findViewById(R.id.new_password_entry_wrapper);
|
||||||
@@ -102,13 +114,12 @@ public class ChangePasswordActivity extends BriarActivity
|
|||||||
String firstPassword = newPassword.getText().toString();
|
String firstPassword = newPassword.getText().toString();
|
||||||
String secondPassword = newPasswordConfirmation.getText().toString();
|
String secondPassword = newPasswordConfirmation.getText().toString();
|
||||||
boolean passwordsMatch = firstPassword.equals(secondPassword);
|
boolean passwordsMatch = firstPassword.equals(secondPassword);
|
||||||
float strength =
|
float strength = viewModel.estimatePasswordStrength(firstPassword);
|
||||||
passwordController.estimatePasswordStrength(firstPassword);
|
|
||||||
strengthMeter.setStrength(strength);
|
strengthMeter.setStrength(strength);
|
||||||
UiUtils.setError(newPasswordEntryWrapper,
|
setError(newPasswordEntryWrapper,
|
||||||
getString(R.string.password_too_weak),
|
getString(R.string.password_too_weak),
|
||||||
firstPassword.length() > 0 && strength < QUITE_WEAK);
|
firstPassword.length() > 0 && strength < QUITE_WEAK);
|
||||||
UiUtils.setError(newPasswordConfirmationWrapper,
|
setError(newPasswordConfirmationWrapper,
|
||||||
getString(R.string.passwords_do_not_match),
|
getString(R.string.passwords_do_not_match),
|
||||||
secondPassword.length() > 0 && !passwordsMatch);
|
secondPassword.length() > 0 && !passwordsMatch);
|
||||||
changePasswordButton.setEnabled(
|
changePasswordButton.setEnabled(
|
||||||
@@ -127,32 +138,34 @@ public class ChangePasswordActivity extends BriarActivity
|
|||||||
// Replace the button with a progress bar
|
// Replace the button with a progress bar
|
||||||
changePasswordButton.setVisibility(INVISIBLE);
|
changePasswordButton.setVisibility(INVISIBLE);
|
||||||
progress.setVisibility(VISIBLE);
|
progress.setVisibility(VISIBLE);
|
||||||
passwordController.changePassword(currentPassword.getText().toString(),
|
|
||||||
newPassword.getText().toString(),
|
String curPwd = currentPassword.getText().toString();
|
||||||
new UiResultHandler<Boolean>(this) {
|
String newPwd = newPassword.getText().toString();
|
||||||
@Override
|
viewModel.changePassword(curPwd, newPwd).observeEvent(this, result -> {
|
||||||
public void onResultUi(@NonNull Boolean result) {
|
if (result == SUCCESS) {
|
||||||
if (result) {
|
Toast.makeText(ChangePasswordActivity.this,
|
||||||
Toast.makeText(ChangePasswordActivity.this,
|
R.string.password_changed,
|
||||||
R.string.password_changed,
|
LENGTH_LONG).show();
|
||||||
Toast.LENGTH_LONG).show();
|
setResult(RESULT_OK);
|
||||||
setResult(RESULT_OK);
|
supportFinishAfterTransition();
|
||||||
supportFinishAfterTransition();
|
} else {
|
||||||
} else {
|
tryAgain(result);
|
||||||
tryAgain();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void tryAgain() {
|
private void tryAgain(DecryptionResult result) {
|
||||||
UiUtils.setError(currentPasswordEntryWrapper,
|
|
||||||
getString(R.string.try_again), true);
|
|
||||||
changePasswordButton.setVisibility(VISIBLE);
|
changePasswordButton.setVisibility(VISIBLE);
|
||||||
progress.setVisibility(INVISIBLE);
|
progress.setVisibility(INVISIBLE);
|
||||||
currentPassword.setText("");
|
if (result == KEY_STRENGTHENER_ERROR) {
|
||||||
|
createKeyStrengthenerErrorDialog(this).show();
|
||||||
// show the keyboard again
|
} else {
|
||||||
showSoftKeyboard(currentPassword);
|
setError(currentPasswordEntryWrapper,
|
||||||
|
getString(R.string.try_again), true);
|
||||||
|
currentPassword.setText("");
|
||||||
|
// show the keyboard again
|
||||||
|
showSoftKeyboard(currentPassword);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
package org.briarproject.briar.android.login;
|
|
||||||
|
|
||||||
import org.briarproject.bramble.api.account.AccountManager;
|
|
||||||
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator;
|
|
||||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
|
||||||
import org.briarproject.briar.android.controller.handler.ResultHandler;
|
|
||||||
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
@NotNullByDefault
|
|
||||||
public class ChangePasswordControllerImpl implements ChangePasswordController {
|
|
||||||
|
|
||||||
protected final AccountManager accountManager;
|
|
||||||
protected final Executor ioExecutor;
|
|
||||||
private final PasswordStrengthEstimator strengthEstimator;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
ChangePasswordControllerImpl(AccountManager accountManager,
|
|
||||||
@IoExecutor Executor ioExecutor,
|
|
||||||
PasswordStrengthEstimator strengthEstimator) {
|
|
||||||
this.accountManager = accountManager;
|
|
||||||
this.ioExecutor = ioExecutor;
|
|
||||||
this.strengthEstimator = strengthEstimator;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float estimatePasswordStrength(String password) {
|
|
||||||
return strengthEstimator.estimateStrength(password);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void changePassword(String oldPassword, String newPassword,
|
|
||||||
ResultHandler<Boolean> resultHandler) {
|
|
||||||
ioExecutor.execute(() -> {
|
|
||||||
boolean changed =
|
|
||||||
accountManager.changePassword(oldPassword, newPassword);
|
|
||||||
resultHandler.onResult(changed);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package org.briarproject.briar.android.login;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.account.AccountManager;
|
||||||
|
import org.briarproject.bramble.api.crypto.DecryptionException;
|
||||||
|
import org.briarproject.bramble.api.crypto.DecryptionResult;
|
||||||
|
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator;
|
||||||
|
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.briar.android.viewmodel.LiveEvent;
|
||||||
|
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel;
|
||||||
|
|
||||||
|
import static org.briarproject.bramble.api.crypto.DecryptionResult.SUCCESS;
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
|
public class ChangePasswordViewModel extends ViewModel {
|
||||||
|
|
||||||
|
private final AccountManager accountManager;
|
||||||
|
private final Executor ioExecutor;
|
||||||
|
private final PasswordStrengthEstimator strengthEstimator;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ChangePasswordViewModel(AccountManager accountManager,
|
||||||
|
@IoExecutor Executor ioExecutor,
|
||||||
|
PasswordStrengthEstimator strengthEstimator) {
|
||||||
|
this.accountManager = accountManager;
|
||||||
|
this.ioExecutor = ioExecutor;
|
||||||
|
this.strengthEstimator = strengthEstimator;
|
||||||
|
}
|
||||||
|
|
||||||
|
float estimatePasswordStrength(String password) {
|
||||||
|
return strengthEstimator.estimateStrength(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
LiveEvent<DecryptionResult> changePassword(String oldPassword,
|
||||||
|
String newPassword) {
|
||||||
|
MutableLiveEvent<DecryptionResult> result = new MutableLiveEvent<>();
|
||||||
|
ioExecutor.execute(() -> {
|
||||||
|
try {
|
||||||
|
accountManager.changePassword(oldPassword, newPassword);
|
||||||
|
result.postEvent(SUCCESS);
|
||||||
|
} catch (DecryptionException e) {
|
||||||
|
result.postEvent(e.getDecryptionResult());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package org.briarproject.briar.android.login;
|
||||||
|
|
||||||
|
import org.briarproject.briar.android.viewmodel.ViewModelKey;
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel;
|
||||||
|
import dagger.Binds;
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.multibindings.IntoMap;
|
||||||
|
|
||||||
|
@Module
|
||||||
|
public abstract class LoginModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ViewModelKey(StartupViewModel.class)
|
||||||
|
abstract ViewModel bindStartupViewModel(StartupViewModel viewModel);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ViewModelKey(ChangePasswordViewModel.class)
|
||||||
|
abstract ViewModel bindChangePasswordViewModel(
|
||||||
|
ChangePasswordViewModel viewModel);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package org.briarproject.briar.android.login;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.briar.R;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
|
||||||
|
import static androidx.core.content.ContextCompat.getColor;
|
||||||
|
import static androidx.core.content.ContextCompat.getDrawable;
|
||||||
|
import static androidx.core.graphics.drawable.DrawableCompat.setTint;
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
|
class LoginUtils {
|
||||||
|
|
||||||
|
static AlertDialog createKeyStrengthenerErrorDialog(Context ctx) {
|
||||||
|
AlertDialog.Builder builder =
|
||||||
|
new AlertDialog.Builder(ctx, R.style.BriarDialogTheme);
|
||||||
|
Drawable icon = getDrawable(ctx, R.drawable.alerts_and_states_error);
|
||||||
|
setTint(requireNonNull(icon), getColor(ctx, R.color.color_primary));
|
||||||
|
builder.setIcon(icon);
|
||||||
|
builder.setTitle(R.string.dialog_title_cannot_check_password);
|
||||||
|
builder.setMessage(R.string.dialog_message_cannot_check_password);
|
||||||
|
builder.setPositiveButton(R.string.ok, null);
|
||||||
|
return builder.create();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import android.widget.ProgressBar;
|
|||||||
import com.google.android.material.textfield.TextInputEditText;
|
import com.google.android.material.textfield.TextInputEditText;
|
||||||
import com.google.android.material.textfield.TextInputLayout;
|
import com.google.android.material.textfield.TextInputLayout;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.crypto.DecryptionResult;
|
||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
@@ -28,6 +29,9 @@ import androidx.lifecycle.ViewModelProviders;
|
|||||||
import static android.view.View.INVISIBLE;
|
import static android.view.View.INVISIBLE;
|
||||||
import static android.view.View.VISIBLE;
|
import static android.view.View.VISIBLE;
|
||||||
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
|
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
|
||||||
|
import static org.briarproject.bramble.api.crypto.DecryptionResult.KEY_STRENGTHENER_ERROR;
|
||||||
|
import static org.briarproject.bramble.api.crypto.DecryptionResult.SUCCESS;
|
||||||
|
import static org.briarproject.briar.android.login.LoginUtils.createKeyStrengthenerErrorDialog;
|
||||||
import static org.briarproject.briar.android.util.UiUtils.enterPressed;
|
import static org.briarproject.briar.android.util.UiUtils.enterPressed;
|
||||||
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
|
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
|
||||||
import static org.briarproject.briar.android.util.UiUtils.setError;
|
import static org.briarproject.briar.android.util.UiUtils.setError;
|
||||||
@@ -58,12 +62,13 @@ public class PasswordFragment extends BaseFragment implements TextWatcher {
|
|||||||
@Nullable ViewGroup container,
|
@Nullable ViewGroup container,
|
||||||
@Nullable Bundle savedInstanceState) {
|
@Nullable Bundle savedInstanceState) {
|
||||||
View v = inflater.inflate(R.layout.fragment_password, container,
|
View v = inflater.inflate(R.layout.fragment_password, container,
|
||||||
false);
|
false);
|
||||||
|
|
||||||
viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)
|
viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)
|
||||||
.get(StartupViewModel.class);
|
.get(StartupViewModel.class);
|
||||||
viewModel.getPasswordValidated().observeEvent(this, valid -> {
|
|
||||||
if (!valid) onPasswordInvalid();
|
viewModel.getPasswordValidated().observeEvent(this, result -> {
|
||||||
|
if (result != SUCCESS) onPasswordInvalid(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
signInButton = v.findViewById(R.id.btn_sign_in);
|
signInButton = v.findViewById(R.id.btn_sign_in);
|
||||||
@@ -107,18 +112,20 @@ public class PasswordFragment extends BaseFragment implements TextWatcher {
|
|||||||
viewModel.validatePassword(password.getText().toString());
|
viewModel.validatePassword(password.getText().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onPasswordInvalid() {
|
private void onPasswordInvalid(DecryptionResult result) {
|
||||||
setError(input, getString(R.string.try_again), true);
|
|
||||||
signInButton.setVisibility(VISIBLE);
|
signInButton.setVisibility(VISIBLE);
|
||||||
progress.setVisibility(INVISIBLE);
|
progress.setVisibility(INVISIBLE);
|
||||||
password.setText(null);
|
if (result == KEY_STRENGTHENER_ERROR) {
|
||||||
|
createKeyStrengthenerErrorDialog(requireContext()).show();
|
||||||
// show the keyboard again
|
} else {
|
||||||
showSoftKeyboard(password);
|
setError(input, getString(R.string.try_again), true);
|
||||||
|
password.setText(null);
|
||||||
|
// show the keyboard again
|
||||||
|
showSoftKeyboard(password);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onForgottenPasswordClick() {
|
private void onForgottenPasswordClick() {
|
||||||
// TODO Encapsulate the dialog in a re-usable fragment
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext(),
|
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext(),
|
||||||
R.style.BriarDialogTheme);
|
R.style.BriarDialogTheme);
|
||||||
builder.setTitle(R.string.dialog_title_lost_password);
|
builder.setTitle(R.string.dialog_title_lost_password);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package org.briarproject.briar.android.login;
|
|||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.account.AccountManager;
|
import org.briarproject.bramble.api.account.AccountManager;
|
||||||
|
import org.briarproject.bramble.api.crypto.DecryptionException;
|
||||||
|
import org.briarproject.bramble.api.crypto.DecryptionResult;
|
||||||
import org.briarproject.bramble.api.event.Event;
|
import org.briarproject.bramble.api.event.Event;
|
||||||
import org.briarproject.bramble.api.event.EventBus;
|
import org.briarproject.bramble.api.event.EventBus;
|
||||||
import org.briarproject.bramble.api.event.EventListener;
|
import org.briarproject.bramble.api.event.EventListener;
|
||||||
@@ -24,6 +26,7 @@ import androidx.lifecycle.AndroidViewModel;
|
|||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
|
import static org.briarproject.bramble.api.crypto.DecryptionResult.SUCCESS;
|
||||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.COMPACTING_DATABASE;
|
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.COMPACTING_DATABASE;
|
||||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.MIGRATING_DATABASE;
|
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.MIGRATING_DATABASE;
|
||||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STARTING_SERVICES;
|
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STARTING_SERVICES;
|
||||||
@@ -46,7 +49,7 @@ public class StartupViewModel extends AndroidViewModel
|
|||||||
@IoExecutor
|
@IoExecutor
|
||||||
private final Executor ioExecutor;
|
private final Executor ioExecutor;
|
||||||
|
|
||||||
private final MutableLiveEvent<Boolean> passwordValidated =
|
private final MutableLiveEvent<DecryptionResult> passwordValidated =
|
||||||
new MutableLiveEvent<>();
|
new MutableLiveEvent<>();
|
||||||
private final MutableLiveEvent<Boolean> accountDeleted =
|
private final MutableLiveEvent<Boolean> accountDeleted =
|
||||||
new MutableLiveEvent<>();
|
new MutableLiveEvent<>();
|
||||||
@@ -105,13 +108,17 @@ public class StartupViewModel extends AndroidViewModel
|
|||||||
|
|
||||||
void validatePassword(String password) {
|
void validatePassword(String password) {
|
||||||
ioExecutor.execute(() -> {
|
ioExecutor.execute(() -> {
|
||||||
boolean signedIn = accountManager.signIn(password);
|
try {
|
||||||
passwordValidated.postEvent(signedIn);
|
accountManager.signIn(password);
|
||||||
if (signedIn) state.postValue(SIGNED_IN);
|
passwordValidated.postEvent(SUCCESS);
|
||||||
|
state.postValue(SIGNED_IN);
|
||||||
|
} catch (DecryptionException e) {
|
||||||
|
passwordValidated.postEvent(e.getDecryptionResult());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
LiveEvent<Boolean> getPasswordValidated() {
|
LiveEvent<DecryptionResult> getPasswordValidated() {
|
||||||
return passwordValidated;
|
return passwordValidated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ public class UiUtils {
|
|||||||
/**
|
/**
|
||||||
* Same as {@link #observeOnce(LiveData, LifecycleOwner, Observer)},
|
* Same as {@link #observeOnce(LiveData, LifecycleOwner, Observer)},
|
||||||
* but without a {@link LifecycleOwner}.
|
* but without a {@link LifecycleOwner}.
|
||||||
*
|
* <p>
|
||||||
* Warning: Do NOT call from objects that have a lifecycle.
|
* Warning: Do NOT call from objects that have a lifecycle.
|
||||||
*/
|
*/
|
||||||
@UiThread
|
@UiThread
|
||||||
@@ -401,5 +401,4 @@ public class UiUtils {
|
|||||||
return ctx.getResources().getConfiguration().getLayoutDirection() ==
|
return ctx.getResources().getConfiguration().getLayoutDirection() ==
|
||||||
LAYOUT_DIRECTION_RTL;
|
LAYOUT_DIRECTION_RTL;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import org.briarproject.briar.android.contact.add.remote.AddContactViewModel;
|
|||||||
import org.briarproject.briar.android.contact.add.remote.PendingContactListViewModel;
|
import org.briarproject.briar.android.contact.add.remote.PendingContactListViewModel;
|
||||||
import org.briarproject.briar.android.conversation.ConversationViewModel;
|
import org.briarproject.briar.android.conversation.ConversationViewModel;
|
||||||
import org.briarproject.briar.android.conversation.ImageViewModel;
|
import org.briarproject.briar.android.conversation.ImageViewModel;
|
||||||
import org.briarproject.briar.android.login.StartupViewModel;
|
|
||||||
|
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
@@ -17,11 +16,6 @@ import dagger.multibindings.IntoMap;
|
|||||||
@Module
|
@Module
|
||||||
public abstract class ViewModelModule {
|
public abstract class ViewModelModule {
|
||||||
|
|
||||||
@Binds
|
|
||||||
@IntoMap
|
|
||||||
@ViewModelKey(StartupViewModel.class)
|
|
||||||
abstract ViewModel bindStartupViewModel(StartupViewModel startupViewModel);
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@ViewModelKey(ConversationViewModel.class)
|
@ViewModelKey(ConversationViewModel.class)
|
||||||
|
|||||||
@@ -32,6 +32,8 @@
|
|||||||
<!-- Login -->
|
<!-- Login -->
|
||||||
<string name="enter_password">Password</string>
|
<string name="enter_password">Password</string>
|
||||||
<string name="try_again">Wrong password, try again</string>
|
<string name="try_again">Wrong password, try again</string>
|
||||||
|
<string name="dialog_title_cannot_check_password">Cannot Check Password</string>
|
||||||
|
<string name="dialog_message_cannot_check_password">Briar cannot check your password. Please try rebooting your device to solve this problem.</string>
|
||||||
<string name="sign_in_button">Sign In</string>
|
<string name="sign_in_button">Sign In</string>
|
||||||
<string name="forgotten_password">I have forgotten my password</string>
|
<string name="forgotten_password">I have forgotten my password</string>
|
||||||
<string name="dialog_title_lost_password">Lost Password</string>
|
<string name="dialog_title_lost_password">Lost Password</string>
|
||||||
|
|||||||
@@ -5,28 +5,30 @@ import android.widget.EditText;
|
|||||||
|
|
||||||
import com.google.android.material.textfield.TextInputLayout;
|
import com.google.android.material.textfield.TextInputLayout;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.crypto.DecryptionResult;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.TestBriarApplication;
|
import org.briarproject.briar.android.TestBriarApplication;
|
||||||
import org.briarproject.briar.android.controller.handler.ResultHandler;
|
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.ArgumentCaptor;
|
|
||||||
import org.mockito.Captor;
|
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.Mockito;
|
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
import org.robolectric.Robolectric;
|
import org.robolectric.Robolectric;
|
||||||
import org.robolectric.RobolectricTestRunner;
|
import org.robolectric.RobolectricTestRunner;
|
||||||
import org.robolectric.annotation.Config;
|
import org.robolectric.annotation.Config;
|
||||||
|
|
||||||
import static junit.framework.Assert.assertEquals;
|
import static junit.framework.Assert.assertEquals;
|
||||||
|
import static junit.framework.Assert.assertFalse;
|
||||||
|
import static junit.framework.Assert.assertTrue;
|
||||||
|
import static org.briarproject.bramble.api.crypto.DecryptionResult.SUCCESS;
|
||||||
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.NONE;
|
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.NONE;
|
||||||
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_STRONG;
|
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_STRONG;
|
||||||
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK;
|
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK;
|
||||||
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.STRONG;
|
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.STRONG;
|
||||||
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.WEAK;
|
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.WEAK;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
import static org.mockito.Matchers.anyString;
|
import static org.mockito.Matchers.anyString;
|
||||||
import static org.mockito.Matchers.eq;
|
import static org.mockito.Matchers.eq;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
@@ -37,7 +39,7 @@ import static org.mockito.Mockito.when;
|
|||||||
@Config(sdk = 21, application = TestBriarApplication.class)
|
@Config(sdk = 21, application = TestBriarApplication.class)
|
||||||
public class ChangePasswordActivityTest {
|
public class ChangePasswordActivityTest {
|
||||||
|
|
||||||
private TestChangePasswordActivity changePasswordActivity;
|
private ChangePasswordActivity changePasswordActivity;
|
||||||
private TextInputLayout passwordConfirmationWrapper;
|
private TextInputLayout passwordConfirmationWrapper;
|
||||||
private EditText currentPassword;
|
private EditText currentPassword;
|
||||||
private EditText newPassword;
|
private EditText newPassword;
|
||||||
@@ -46,15 +48,14 @@ public class ChangePasswordActivityTest {
|
|||||||
private Button changePasswordButton;
|
private Button changePasswordButton;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ChangePasswordController passwordController;
|
private ChangePasswordViewModel viewModel;
|
||||||
@Captor
|
|
||||||
private ArgumentCaptor<ResultHandler<Boolean>> resultCaptor;
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
MockitoAnnotations.initMocks(this);
|
MockitoAnnotations.initMocks(this);
|
||||||
changePasswordActivity =
|
changePasswordActivity =
|
||||||
Robolectric.setupActivity(TestChangePasswordActivity.class);
|
Robolectric.setupActivity(ChangePasswordActivity.class);
|
||||||
|
changePasswordActivity.viewModel = viewModel;
|
||||||
passwordConfirmationWrapper = changePasswordActivity
|
passwordConfirmationWrapper = changePasswordActivity
|
||||||
.findViewById(R.id.new_password_confirm_wrapper);
|
.findViewById(R.id.new_password_confirm_wrapper);
|
||||||
currentPassword = changePasswordActivity
|
currentPassword = changePasswordActivity
|
||||||
@@ -81,7 +82,7 @@ public class ChangePasswordActivityTest {
|
|||||||
// Password mismatch
|
// Password mismatch
|
||||||
newPassword.setText("really.safe.password");
|
newPassword.setText("really.safe.password");
|
||||||
newPasswordConfirmation.setText("really.safe.pass");
|
newPasswordConfirmation.setText("really.safe.pass");
|
||||||
assertEquals(changePasswordButton.isEnabled(), false);
|
assertFalse(changePasswordButton.isEnabled());
|
||||||
assertEquals(passwordConfirmationWrapper.getError(),
|
assertEquals(passwordConfirmationWrapper.getError(),
|
||||||
changePasswordActivity
|
changePasswordActivity
|
||||||
.getString(R.string.passwords_do_not_match));
|
.getString(R.string.passwords_do_not_match));
|
||||||
@@ -89,70 +90,59 @@ public class ChangePasswordActivityTest {
|
|||||||
newPassword.setText("really.safe.pass");
|
newPassword.setText("really.safe.pass");
|
||||||
newPasswordConfirmation.setText("really.safe.pass");
|
newPasswordConfirmation.setText("really.safe.pass");
|
||||||
// Confirm that the password mismatch error message is not visible
|
// Confirm that the password mismatch error message is not visible
|
||||||
Assert.assertNotEquals(passwordConfirmationWrapper.getError(),
|
assertNotEquals(passwordConfirmationWrapper.getError(),
|
||||||
changePasswordActivity
|
changePasswordActivity
|
||||||
.getString(R.string.passwords_do_not_match));
|
.getString(R.string.passwords_do_not_match));
|
||||||
// Nick has not been set, expect the button to be disabled
|
// Nick has not been set, expect the button to be disabled
|
||||||
assertEquals(changePasswordButton.isEnabled(), false);
|
assertFalse(changePasswordButton.isEnabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testChangePasswordUI() {
|
public void testChangePasswordUI() {
|
||||||
changePasswordActivity.setPasswordController(passwordController);
|
|
||||||
// Mock strong password strength answer
|
// Mock strong password strength answer
|
||||||
when(passwordController.estimatePasswordStrength(anyString()))
|
when(viewModel.estimatePasswordStrength(anyString()))
|
||||||
.thenReturn(STRONG);
|
.thenReturn(STRONG);
|
||||||
|
// Mock changing the password
|
||||||
|
MutableLiveEvent<DecryptionResult> result = new MutableLiveEvent<>();
|
||||||
|
when(viewModel.changePassword(anyString(), anyString()))
|
||||||
|
.thenReturn(result);
|
||||||
String curPass = "old.password";
|
String curPass = "old.password";
|
||||||
String safePass = "really.safe.password";
|
String safePass = "really.safe.password";
|
||||||
currentPassword.setText(curPass);
|
currentPassword.setText(curPass);
|
||||||
newPassword.setText(safePass);
|
newPassword.setText(safePass);
|
||||||
newPasswordConfirmation.setText(safePass);
|
newPasswordConfirmation.setText(safePass);
|
||||||
// Confirm that the create account button is clickable
|
// Confirm that the create account button is clickable
|
||||||
assertEquals(changePasswordButton.isEnabled(), true);
|
assertTrue(changePasswordButton.isEnabled());
|
||||||
changePasswordButton.performClick();
|
changePasswordButton.performClick();
|
||||||
// Verify that the controller's method was called with the correct
|
// Verify that the view model was called with the correct params
|
||||||
// params and get the callback
|
verify(viewModel, times(1)).changePassword(eq(curPass), eq(safePass));
|
||||||
verify(passwordController, times(1))
|
// Return the result
|
||||||
.changePassword(eq(curPass), eq(safePass),
|
result.postEvent(SUCCESS);
|
||||||
resultCaptor.capture());
|
assertTrue(changePasswordActivity.isFinishing());
|
||||||
// execute the callbacks
|
|
||||||
resultCaptor.getValue().onResult(true);
|
|
||||||
assertEquals(changePasswordActivity.isFinishing(), true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testStrengthMeterUI() {
|
public void testStrengthMeterUI() {
|
||||||
Assert.assertNotNull(changePasswordActivity);
|
Assert.assertNotNull(changePasswordActivity);
|
||||||
// replace the password controller with our mocked copy
|
|
||||||
changePasswordActivity.setPasswordController(passwordController);
|
|
||||||
// Mock answers for UI testing only
|
// Mock answers for UI testing only
|
||||||
when(passwordController.estimatePasswordStrength("strong")).thenReturn(
|
when(viewModel.estimatePasswordStrength("strong")).thenReturn(STRONG);
|
||||||
STRONG);
|
when(viewModel.estimatePasswordStrength("qstrong"))
|
||||||
when(passwordController.estimatePasswordStrength("qstrong")).thenReturn(
|
.thenReturn(QUITE_STRONG);
|
||||||
QUITE_STRONG);
|
when(viewModel.estimatePasswordStrength("qweak"))
|
||||||
when(passwordController.estimatePasswordStrength("qweak")).thenReturn(
|
.thenReturn(QUITE_WEAK);
|
||||||
QUITE_WEAK);
|
when(viewModel.estimatePasswordStrength("weak")).thenReturn(WEAK);
|
||||||
when(passwordController.estimatePasswordStrength("weak")).thenReturn(
|
when(viewModel.estimatePasswordStrength("empty")).thenReturn(NONE);
|
||||||
WEAK);
|
|
||||||
when(passwordController.estimatePasswordStrength("empty")).thenReturn(
|
|
||||||
NONE);
|
|
||||||
// Test the meters progress and color for several values
|
// Test the meters progress and color for several values
|
||||||
testStrengthMeter("strong", STRONG, StrengthMeter.GREEN);
|
testStrengthMeter("strong", STRONG, StrengthMeter.GREEN);
|
||||||
Mockito.verify(passwordController, Mockito.times(1))
|
verify(viewModel, times(1)).estimatePasswordStrength(eq("strong"));
|
||||||
.estimatePasswordStrength(eq("strong"));
|
|
||||||
testStrengthMeter("qstrong", QUITE_STRONG, StrengthMeter.LIME);
|
testStrengthMeter("qstrong", QUITE_STRONG, StrengthMeter.LIME);
|
||||||
Mockito.verify(passwordController, Mockito.times(1))
|
verify(viewModel, times(1)).estimatePasswordStrength(eq("qstrong"));
|
||||||
.estimatePasswordStrength(eq("qstrong"));
|
|
||||||
testStrengthMeter("qweak", QUITE_WEAK, StrengthMeter.YELLOW);
|
testStrengthMeter("qweak", QUITE_WEAK, StrengthMeter.YELLOW);
|
||||||
Mockito.verify(passwordController, Mockito.times(1))
|
verify(viewModel, times(1)).estimatePasswordStrength(eq("qweak"));
|
||||||
.estimatePasswordStrength(eq("qweak"));
|
|
||||||
testStrengthMeter("weak", WEAK, StrengthMeter.ORANGE);
|
testStrengthMeter("weak", WEAK, StrengthMeter.ORANGE);
|
||||||
Mockito.verify(passwordController, Mockito.times(1))
|
verify(viewModel, times(1)).estimatePasswordStrength(eq("weak"));
|
||||||
.estimatePasswordStrength(eq("weak"));
|
|
||||||
// Not sure this should be the correct behaviour on an empty input ?
|
// Not sure this should be the correct behaviour on an empty input ?
|
||||||
testStrengthMeter("empty", NONE, StrengthMeter.RED);
|
testStrengthMeter("empty", NONE, StrengthMeter.RED);
|
||||||
Mockito.verify(passwordController, Mockito.times(1))
|
verify(viewModel, times(1)).estimatePasswordStrength(eq("empty"));
|
||||||
.estimatePasswordStrength(eq("empty"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
package org.briarproject.briar.android.login;
|
|
||||||
|
|
||||||
import org.briarproject.bramble.api.account.AccountManager;
|
|
||||||
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator;
|
|
||||||
import org.briarproject.bramble.test.BrambleMockTestCase;
|
|
||||||
import org.briarproject.bramble.test.ImmediateExecutor;
|
|
||||||
import org.jmock.Expectations;
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
import static junit.framework.Assert.assertFalse;
|
|
||||||
import static junit.framework.Assert.assertTrue;
|
|
||||||
import static org.briarproject.bramble.util.StringUtils.getRandomString;
|
|
||||||
|
|
||||||
public class ChangePasswordControllerImplTest extends BrambleMockTestCase {
|
|
||||||
|
|
||||||
private final AccountManager accountManager =
|
|
||||||
context.mock(AccountManager.class);
|
|
||||||
private final PasswordStrengthEstimator estimator =
|
|
||||||
context.mock(PasswordStrengthEstimator.class);
|
|
||||||
|
|
||||||
private final Executor ioExecutor = new ImmediateExecutor();
|
|
||||||
|
|
||||||
private final String oldPassword = getRandomString(10);
|
|
||||||
private final String newPassword = getRandomString(10);
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testChangePasswordReturnsTrue() {
|
|
||||||
context.checking(new Expectations() {{
|
|
||||||
oneOf(accountManager).changePassword(oldPassword, newPassword);
|
|
||||||
will(returnValue(true));
|
|
||||||
}});
|
|
||||||
|
|
||||||
ChangePasswordControllerImpl p = new ChangePasswordControllerImpl(accountManager,
|
|
||||||
ioExecutor, estimator);
|
|
||||||
|
|
||||||
AtomicBoolean capturedResult = new AtomicBoolean(false);
|
|
||||||
p.changePassword(oldPassword, newPassword, capturedResult::set);
|
|
||||||
assertTrue(capturedResult.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testChangePasswordReturnsFalseIfOldPasswordIsWrong() {
|
|
||||||
context.checking(new Expectations() {{
|
|
||||||
oneOf(accountManager).changePassword(oldPassword, newPassword);
|
|
||||||
will(returnValue(false));
|
|
||||||
}});
|
|
||||||
|
|
||||||
ChangePasswordControllerImpl p = new ChangePasswordControllerImpl(accountManager,
|
|
||||||
ioExecutor, estimator);
|
|
||||||
|
|
||||||
AtomicBoolean capturedResult = new AtomicBoolean(true);
|
|
||||||
p.changePassword(oldPassword, newPassword, capturedResult::set);
|
|
||||||
assertFalse(capturedResult.get());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package org.briarproject.briar.android.login;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class exposes the PasswordController and offers the possibility to
|
|
||||||
* replace it.
|
|
||||||
*/
|
|
||||||
public class TestChangePasswordActivity extends ChangePasswordActivity {
|
|
||||||
|
|
||||||
public void setPasswordController(
|
|
||||||
ChangePasswordController passwordController) {
|
|
||||||
this.passwordController = passwordController;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ import com.github.ajalt.clikt.core.UsageError
|
|||||||
import com.github.ajalt.clikt.output.TermUi.echo
|
import com.github.ajalt.clikt.output.TermUi.echo
|
||||||
import com.github.ajalt.clikt.output.TermUi.prompt
|
import com.github.ajalt.clikt.output.TermUi.prompt
|
||||||
import org.briarproject.bramble.api.account.AccountManager
|
import org.briarproject.bramble.api.account.AccountManager
|
||||||
|
import org.briarproject.bramble.api.crypto.DecryptionException
|
||||||
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator
|
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator
|
||||||
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK
|
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK
|
||||||
import org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH
|
import org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH
|
||||||
@@ -34,7 +35,9 @@ constructor(
|
|||||||
} else {
|
} else {
|
||||||
val password = prompt("Password", hideInput = true)
|
val password = prompt("Password", hideInput = true)
|
||||||
?: throw UsageError("Could not get password. Is STDIN connected?")
|
?: throw UsageError("Could not get password. Is STDIN connected?")
|
||||||
if (!accountManager.signIn(password)) {
|
try {
|
||||||
|
accountManager.signIn(password)
|
||||||
|
} catch (e : DecryptionException) {
|
||||||
echo("Error: Password invalid")
|
echo("Error: Password invalid")
|
||||||
exitProcess(1)
|
exitProcess(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.briarproject.briar.headless
|
package org.briarproject.briar.headless
|
||||||
|
|
||||||
import org.briarproject.bramble.api.account.AccountManager
|
import org.briarproject.bramble.api.account.AccountManager
|
||||||
|
import org.briarproject.bramble.api.crypto.DecryptionException
|
||||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager
|
import org.briarproject.bramble.api.lifecycle.LifecycleManager
|
||||||
import javax.annotation.concurrent.Immutable
|
import javax.annotation.concurrent.Immutable
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -23,7 +24,9 @@ constructor(
|
|||||||
accountManager.deleteAccount()
|
accountManager.deleteAccount()
|
||||||
}
|
}
|
||||||
accountManager.createAccount(user, pass)
|
accountManager.createAccount(user, pass)
|
||||||
if (!accountManager.signIn(pass)) {
|
try {
|
||||||
|
accountManager.signIn(pass)
|
||||||
|
} catch (e: DecryptionException) {
|
||||||
throw AssertionError("Password invalid")
|
throw AssertionError("Password invalid")
|
||||||
}
|
}
|
||||||
val dbKey = accountManager.databaseKey ?: throw AssertionError()
|
val dbKey = accountManager.databaseKey ?: throw AssertionError()
|
||||||
|
|||||||
Reference in New Issue
Block a user