Repo created
This commit is contained in:
commit
3c8e58604e
646 changed files with 69135 additions and 0 deletions
1
app/.gitignore
vendored
Normal file
1
app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
225
app/build.gradle
Normal file
225
app/build.gradle
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.google.protobuf'
|
||||
apply plugin: 'dagger.hilt.android.plugin'
|
||||
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
||||
|
||||
def getCmdOutput = { cmd ->
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine cmd
|
||||
standardOutput = stdout
|
||||
}
|
||||
return stdout.toString().trim()
|
||||
}
|
||||
|
||||
def getGitHash = { -> return getCmdOutput(["git", "rev-parse", "--short", "HEAD"]) }
|
||||
def getGitBranch = { -> return getCmdOutput(["git", "rev-parse", "--abbrev-ref", "HEAD"]) }
|
||||
|
||||
def packageName = "com.beemdevelopment.aegis"
|
||||
def fileProviderAuthority = "${packageName}.fileprovider"
|
||||
def fileProviderAuthorityDebug = "${packageName}.debug.fileprovider"
|
||||
|
||||
android {
|
||||
compileSdk 35
|
||||
|
||||
namespace packageName
|
||||
|
||||
defaultConfig {
|
||||
applicationId "${packageName}"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 35
|
||||
versionCode 80
|
||||
versionName "3.4.1"
|
||||
multiDexEnabled true
|
||||
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
|
||||
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
|
||||
buildConfigField "java.util.concurrent.atomic.AtomicBoolean", "TEST", "new java.util.concurrent.atomic.AtomicBoolean(false)"
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments = ["room.schemaLocation": "$projectDir/schemas"]
|
||||
}
|
||||
}
|
||||
|
||||
testInstrumentationRunner "com.beemdevelopment.aegis.AegisTestRunner"
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
}
|
||||
|
||||
testOptions {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
|
||||
unitTests {
|
||||
all {
|
||||
maxHeapSize "3g"
|
||||
|
||||
ignoreFailures false
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
||||
|
||||
showExceptions true
|
||||
exceptionFormat "full"
|
||||
showCauses true
|
||||
showStackTraces true
|
||||
}
|
||||
}
|
||||
|
||||
includeAndroidResources true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
manifestPlaceholders = [
|
||||
title: "AegisDev",
|
||||
iconName: "ic_launcher_debug",
|
||||
fileProviderAuthority: "${fileProviderAuthorityDebug}"
|
||||
]
|
||||
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${fileProviderAuthorityDebug}\"")
|
||||
resValue "bool", "pref_secure_screen_default", "false"
|
||||
}
|
||||
release {
|
||||
manifestPlaceholders = [
|
||||
title: "Aegis",
|
||||
iconName: "ic_launcher",
|
||||
fileProviderAuthority: "${fileProviderAuthority}"
|
||||
]
|
||||
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${fileProviderAuthority}\"")
|
||||
resValue "bool", "pref_secure_screen_default", "true"
|
||||
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
// Required to make the APK reproducible
|
||||
aaptOptions {
|
||||
cruncherEnabled = false
|
||||
}
|
||||
defaultConfig {
|
||||
vectorDrawables.generatedDensities = []
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
// R8 doesn't remove these resources, so exclude them manually. This reduces APK size by 4MB.
|
||||
resources {
|
||||
excludes += [
|
||||
'/org/bouncycastle/pqc/**/*.properties',
|
||||
'META-INF/versions/9/OSGI-INF/MANIFEST.MF'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
coreLibraryDesugaringEnabled true
|
||||
}
|
||||
lint {
|
||||
abortOnError true
|
||||
checkDependencies true
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
}
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = 'com.google.protobuf:protoc:3.25.1'
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().each { task ->
|
||||
task.builtins {
|
||||
java {
|
||||
option "lite"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
aboutLibraries {
|
||||
// Tasks for aboutLibraries are not run automatically to keep the build reproducible
|
||||
// To update manually: ./gradlew app:exportLibraryDefinitions -PaboutLibraries.exportPath=src/main/res/raw
|
||||
prettyPrint = true
|
||||
configPath = "app/config"
|
||||
fetchRemoteFunding = false
|
||||
registerAndroidTasks = false
|
||||
exclusionPatterns = [~"javax.annotation.*"]
|
||||
duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def cameraxVersion = '1.4.2'
|
||||
def glideVersion = '4.16.0'
|
||||
def guavaVersion = '33.4.8'
|
||||
def hiltVersion = '2.56.2'
|
||||
def junitVersion = '4.13.2'
|
||||
def libsuVersion = '6.0.0'
|
||||
def roomVersion = '2.7.1'
|
||||
|
||||
annotationProcessor 'androidx.annotation:annotation:1.9.1'
|
||||
annotationProcessor "androidx.room:room-compiler:$roomVersion"
|
||||
annotationProcessor "com.google.dagger:hilt-compiler:$hiltVersion"
|
||||
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'androidx.activity:activity:1.10.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation "androidx.biometric:biometric:1.1.0"
|
||||
implementation "androidx.camera:camera-camera2:$cameraxVersion"
|
||||
implementation "androidx.camera:camera-lifecycle:$cameraxVersion"
|
||||
implementation "androidx.camera:camera-view:$cameraxVersion"
|
||||
implementation 'androidx.core:core:1.16.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
||||
implementation 'androidx.documentfile:documentfile:1.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.9.0'
|
||||
implementation "androidx.preference:preference:1.2.1"
|
||||
implementation 'androidx.recyclerview:recyclerview:1.4.0'
|
||||
implementation "androidx.room:room-runtime:$roomVersion"
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0'
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
implementation "com.google.dagger:hilt-android:$hiltVersion"
|
||||
implementation 'com.github.avito-tech:krop:0.52'
|
||||
implementation "com.github.bumptech.glide:annotations:${glideVersion}"
|
||||
implementation "com.github.bumptech.glide:glide:${glideVersion}"
|
||||
implementation("com.github.bumptech.glide:recyclerview-integration:${glideVersion}") {
|
||||
transitive = false
|
||||
}
|
||||
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
|
||||
implementation "com.google.guava:guava:${guavaVersion}-android"
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:4.31.0'
|
||||
implementation 'com.google.zxing:core:3.5.3'
|
||||
implementation('com.mikepenz:aboutlibraries:11.2.3') {
|
||||
exclude group: 'com.mikepenz', module: 'aboutlibraries-core'
|
||||
}
|
||||
implementation 'com.mikepenz:aboutlibraries-core-android:11.2.3'
|
||||
implementation 'com.nulab-inc:zxcvbn:1.9.0'
|
||||
implementation 'net.lingala.zip4j:zip4j:2.11.5'
|
||||
implementation 'org.bouncycastle:bcprov-jdk18on:1.80'
|
||||
implementation 'org.simpleflatmapper:sfm-csv:8.2.3'
|
||||
|
||||
androidTestAnnotationProcessor "com.google.dagger:hilt-android-compiler:$hiltVersion"
|
||||
androidTestImplementation "com.google.dagger:hilt-android-testing:$hiltVersion"
|
||||
androidTestImplementation 'androidx.test:core:1.6.1'
|
||||
androidTestImplementation 'androidx.test:runner:1.6.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.6.1'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.6.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.6.1'
|
||||
androidTestImplementation "junit:junit:${junitVersion}"
|
||||
androidTestUtil 'androidx.test:orchestrator:1.5.1'
|
||||
|
||||
testImplementation 'androidx.test:core:1.6.1'
|
||||
testImplementation "com.google.guava:guava:${guavaVersion}-jre"
|
||||
testImplementation "junit:junit:${junitVersion}"
|
||||
testImplementation 'org.json:json:20250517'
|
||||
testImplementation 'org.robolectric:robolectric:4.14.1'
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
|
||||
}
|
||||
6
app/config/libraries/krop.json
Normal file
6
app/config/libraries/krop.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"uniqueId": "com.github.avito-tech:krop",
|
||||
"licenses": [
|
||||
"MIT"
|
||||
]
|
||||
}
|
||||
6
app/config/libraries/libsu.json
Normal file
6
app/config/libraries/libsu.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"uniqueId": "com.github.topjohnwu.libsu:.*::regex",
|
||||
"licenses": [
|
||||
"Apache-2.0"
|
||||
]
|
||||
}
|
||||
15
app/config/libraries/textdrawable.json
Normal file
15
app/config/libraries/textdrawable.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"uniqueId": "com.amulyakhare:com.amulyakhare.textdrawable",
|
||||
"funding": [
|
||||
|
||||
],
|
||||
"developers": [
|
||||
|
||||
],
|
||||
"artifactVersion": "1.0.1",
|
||||
"description": "This light-weight library provides images with letter/text like the Gmail app. It extends the Drawable class thus can be used with existing/custom/network ImageView classes. Also included is a fluent interface for creating drawables and a customizable ColorGenerator.",
|
||||
"name": "textdrawable",
|
||||
"licenses": [
|
||||
"MIT"
|
||||
]
|
||||
}
|
||||
23
app/config/libraries/trustedintents.json
Normal file
23
app/config/libraries/trustedintents.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"uniqueId": "info.guardianproject.trustedintents:trustedintents",
|
||||
"funding": [
|
||||
|
||||
],
|
||||
"developers": [
|
||||
{
|
||||
"name": "Guardian Project"
|
||||
}
|
||||
],
|
||||
"artifactVersion": "0.2",
|
||||
"description": "TrustedIntents is a library for flexible trusted interactions between Android apps. It is modeled after Android's `signature` protection level for permissions. The key difference is that the framework allows the trusted signature to be set, rather than requiring to match the current app's signature.",
|
||||
"scm": {
|
||||
"connection": "scm:https://github.com/guardianproject/TrustedIntents.git",
|
||||
"url": "scm:https://github.com/guardianproject/TrustedIntents",
|
||||
"developerConnection": "scm:git@github.com:guardianproject/TrustedIntents.git"
|
||||
},
|
||||
"name": "TrustedIntents",
|
||||
"website": "https://guardianproject.info/code/trustedintents",
|
||||
"licenses": [
|
||||
"3ca920d1875f7ad7ab04a2a331958577"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"hash": "3ca920d1875f7ad7ab04a2a331958577",
|
||||
"url": "https://github.com/guardianproject/TrustedIntents/blob/master/LICENSE.txt",
|
||||
"name": "LGPLv2.1"
|
||||
}
|
||||
15
app/lint.xml
Normal file
15
app/lint.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
<issue id="MissingTranslation" severity="ignore" />
|
||||
<issue id="MissingQuantity" severity="ignore" />
|
||||
<issue id="InvalidPackage">
|
||||
<ignore regexp="X509LDAPCertStoreSpi" />
|
||||
</issue>
|
||||
<issue id="NotificationPermission">
|
||||
<ignore regexp="com.bumptech.glide.request.target.NotificationTarget" />
|
||||
</issue>
|
||||
<issue id="UnusedResources" severity="error">
|
||||
<ignore path="res/raw/aboutlibraries.json" />
|
||||
<ignore regexp="res/mipmap.*/ic_launcher_debug.*.png" />
|
||||
</issue>
|
||||
</lint>
|
||||
10
app/proguard-rules.pro
vendored
Normal file
10
app/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
-keepattributes LineNumberTable,SourceFile
|
||||
-renamesourcefileattribute SourceFile
|
||||
-dontobfuscate
|
||||
|
||||
-keepclasseswithmembers public class androidx.recyclerview.widget.RecyclerView { *; }
|
||||
-keep class com.beemdevelopment.aegis.ui.fragments.preferences.*
|
||||
-keep class com.beemdevelopment.aegis.importers.** { *; }
|
||||
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
|
||||
|
||||
-dontwarn javax.naming.**
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "392278bdb797d013cb2ada67a3b1cc60",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "audit_logs",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `event_type` TEXT NOT NULL, `reference` TEXT, `timestamp` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "_eventType",
|
||||
"columnName": "event_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "_reference",
|
||||
"columnName": "reference",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "_timestamp",
|
||||
"columnName": "timestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '392278bdb797d013cb2ada67a3b1cc60')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.test.espresso.UiController;
|
||||
import androidx.test.espresso.ViewAction;
|
||||
import androidx.test.espresso.matcher.BoundedMatcher;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.crypto.SCryptParameters;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.ui.views.EntryHolder;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.vectors.VaultEntries;
|
||||
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidRule;
|
||||
|
||||
public abstract class AegisTest {
|
||||
public static final String VAULT_PASSWORD = "test";
|
||||
public static final String VAULT_PASSWORD_CHANGED = "test2";
|
||||
public static final String VAULT_BACKUP_PASSWORD = "something";
|
||||
public static final String VAULT_BACKUP_PASSWORD_CHANGED = "something2";
|
||||
|
||||
@Rule
|
||||
public HiltAndroidRule hiltRule = new HiltAndroidRule(this);
|
||||
|
||||
@Rule
|
||||
public final GrantPermissionRule permRule = getGrantPermissionRule();
|
||||
|
||||
@Inject
|
||||
protected VaultManager _vaultManager;
|
||||
|
||||
@Inject
|
||||
protected Preferences _prefs;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
hiltRule.inject();
|
||||
}
|
||||
|
||||
private static GrantPermissionRule getGrantPermissionRule() {
|
||||
List<String> perms = new ArrayList<>();
|
||||
// NOTE: Disabled for now. See issue: #1047
|
||||
/*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
perms.add(Manifest.permission.POST_NOTIFICATIONS);
|
||||
}*/
|
||||
return GrantPermissionRule.grant(perms.toArray(new String[0]));
|
||||
}
|
||||
|
||||
protected AegisApplicationBase getApp() {
|
||||
return (AegisApplicationBase) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();
|
||||
}
|
||||
|
||||
protected VaultRepository initEncryptedVault() {
|
||||
VaultFileCredentials creds = generateCredentials();
|
||||
return initVault(creds, VaultEntries.get());
|
||||
}
|
||||
|
||||
protected VaultRepository initEmptyEncryptedVault() {
|
||||
VaultFileCredentials creds = generateCredentials();
|
||||
return initVault(creds, null);
|
||||
}
|
||||
|
||||
protected VaultRepository initPlainVault() {
|
||||
return initVault(null, VaultEntries.get());
|
||||
}
|
||||
|
||||
protected VaultRepository initEmptyPlainVault() {
|
||||
return initVault(null, null);
|
||||
}
|
||||
|
||||
private VaultRepository initVault(@Nullable VaultFileCredentials creds, @Nullable List<VaultEntry> entries) {
|
||||
VaultRepository vault;
|
||||
try {
|
||||
vault = _vaultManager.initNew(creds);
|
||||
} catch (VaultRepositoryException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
if (entries != null) {
|
||||
for (VaultEntry entry : entries) {
|
||||
_vaultManager.getVault().addEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
_vaultManager.save();
|
||||
} catch (VaultRepositoryException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
_prefs.setIntroDone(true);
|
||||
return vault;
|
||||
}
|
||||
|
||||
protected VaultFileCredentials generateCredentials() {
|
||||
PasswordSlot slot = new PasswordSlot();
|
||||
byte[] salt = CryptoUtils.generateSalt();
|
||||
SCryptParameters scryptParams = new SCryptParameters(
|
||||
CryptoUtils.CRYPTO_SCRYPT_N,
|
||||
CryptoUtils.CRYPTO_SCRYPT_r,
|
||||
CryptoUtils.CRYPTO_SCRYPT_p,
|
||||
salt
|
||||
);
|
||||
|
||||
VaultFileCredentials creds = new VaultFileCredentials();
|
||||
try {
|
||||
SecretKey key = slot.deriveKey(VAULT_PASSWORD.toCharArray(), scryptParams);
|
||||
slot.setKey(creds.getKey(), CryptoUtils.createEncryptCipher(key));
|
||||
} catch (NoSuchAlgorithmException
|
||||
| InvalidKeyException
|
||||
| InvalidAlgorithmParameterException
|
||||
| NoSuchPaddingException
|
||||
| SlotException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
creds.getSlots().add(slot);
|
||||
return creds;
|
||||
}
|
||||
|
||||
protected static <T extends OtpInfo> VaultEntry generateEntry(Class<T> type, String name, String issuer) {
|
||||
return generateEntry(type, name, issuer, 20);
|
||||
}
|
||||
|
||||
protected static <T extends OtpInfo> VaultEntry generateEntry(Class<T> type, String name, String issuer, int secretLength) {
|
||||
byte[] secret = CryptoUtils.generateRandomBytes(secretLength);
|
||||
|
||||
OtpInfo info;
|
||||
try {
|
||||
info = type.getConstructor(byte[].class).newInstance(secret);
|
||||
} catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return new VaultEntry(info, name, issuer);
|
||||
}
|
||||
|
||||
// source: https://stackoverflow.com/a/30338665
|
||||
protected static ViewAction clickChildViewWithId(final int id) {
|
||||
return new ViewAction() {
|
||||
@Override
|
||||
public Matcher<View> getConstraints() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Click on a child view with specified id.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(UiController uiController, View view) {
|
||||
View v = view.findViewById(id);
|
||||
v.performClick();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected static Matcher<RecyclerView.ViewHolder> withOtpType(Class<? extends OtpInfo> otpClass) {
|
||||
return new BoundedMatcher<RecyclerView.ViewHolder, EntryHolder>(EntryHolder.class) {
|
||||
@Override
|
||||
public boolean matchesSafely(EntryHolder holder) {
|
||||
return holder != null
|
||||
&& holder.getEntry() != null
|
||||
&& holder.getEntry().getInfo().getClass().equals(otpClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText(String.format("with otp type '%s'", otpClass.getSimpleName()));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import dagger.hilt.android.testing.CustomTestApplication;
|
||||
|
||||
@CustomTestApplication(AegisApplicationBase.class)
|
||||
public interface AegisTestApplication {
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.runner.AndroidJUnitRunner;
|
||||
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
|
||||
public class AegisTestRunner extends AndroidJUnitRunner {
|
||||
static {
|
||||
BuildConfig.TEST.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Application newApplication(ClassLoader cl, String name, Context context)
|
||||
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
|
||||
return Instrumentation.newApplication(AegisTestApplication_Application.class, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void callApplicationOnCreate(Application app) {
|
||||
Context context = app.getApplicationContext();
|
||||
|
||||
// clear internal storage so that there is no vault file
|
||||
IOUtils.clearDirectory(context.getFilesDir(), false);
|
||||
|
||||
// clear preferences so that the intro is started from MainActivity
|
||||
ApplicationProvider.getApplicationContext().getFilesDir();
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.clear()
|
||||
.apply();
|
||||
|
||||
super.callApplicationOnCreate(app);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,431 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
|
||||
import static androidx.test.espresso.action.ViewActions.pressBack;
|
||||
import static androidx.test.espresso.action.ViewActions.typeText;
|
||||
import static androidx.test.espresso.intent.Intents.intending;
|
||||
import static androidx.test.espresso.intent.matcher.IntentMatchers.isInternal;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions;
|
||||
import androidx.test.espresso.intent.Intents;
|
||||
import androidx.test.espresso.matcher.RootMatchers;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.crypto.MasterKey;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporter;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
|
||||
import com.beemdevelopment.aegis.importers.GoogleAuthUriImporter;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.rules.ScreenshotTestRule;
|
||||
import com.beemdevelopment.aegis.ui.PreferencesActivity;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultBackupManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileException;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotIntegrityException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
import com.beemdevelopment.aegis.vectors.VaultEntries;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.RuleChain;
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
import org.xmlpull.v1.XmlPullParserFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@HiltAndroidTest
|
||||
@SmallTest
|
||||
public class BackupExportTest extends AegisTest {
|
||||
private final ActivityScenarioRule<PreferencesActivity> _activityRule = new ActivityScenarioRule<>(PreferencesActivity.class);
|
||||
|
||||
@Rule
|
||||
public final TestRule testRule = RuleChain.outerRule(_activityRule).around(new ScreenshotTestRule());
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
Intents.init();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
Intents.release();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlainVaultExportPlainJson() {
|
||||
initPlainVault();
|
||||
|
||||
openExportDialog();
|
||||
onView(withId(R.id.checkbox_export_encrypt)).perform(click());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
onView(withId(R.id.checkbox_accept)).perform(click());
|
||||
File file = doExport();
|
||||
|
||||
readVault(file, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlainVaultExportPlainTxt() {
|
||||
initPlainVault();
|
||||
|
||||
openExportDialog();
|
||||
onView(withId(R.id.checkbox_export_encrypt)).perform(click());
|
||||
onView(withId(R.id.dropdown_export_format)).perform(click());
|
||||
onView(withText(R.string.export_format_google_auth_uri)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
onView(withId(R.id.checkbox_accept)).perform(click());
|
||||
File file = doExport();
|
||||
|
||||
readTxtExport(file);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlainVaultExportEncryptedJson() {
|
||||
initPlainVault();
|
||||
|
||||
openExportDialog();
|
||||
File file = doExport();
|
||||
|
||||
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
readVault(file, VAULT_PASSWORD);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncryptedVaultExportPlainJson() {
|
||||
initEncryptedVault();
|
||||
|
||||
openExportDialog();
|
||||
onView(withId(R.id.checkbox_export_encrypt)).perform(click());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
onView(withId(R.id.checkbox_accept)).perform(click());
|
||||
File file = doExport();
|
||||
|
||||
readVault(file, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncryptedVaultExportPlainTxt() {
|
||||
initEncryptedVault();
|
||||
|
||||
openExportDialog();
|
||||
onView(withId(R.id.checkbox_export_encrypt)).perform(click());
|
||||
onView(withId(R.id.dropdown_export_format)).perform(click());
|
||||
onView(withText(R.string.export_format_google_auth_uri)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
onView(withId(R.id.checkbox_accept)).perform(click());
|
||||
File file = doExport();
|
||||
|
||||
readTxtExport(file);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncryptedVaultExportEncryptedJson() {
|
||||
initEncryptedVault();
|
||||
|
||||
openExportDialog();
|
||||
File file = doExport();
|
||||
|
||||
readVault(file, VAULT_PASSWORD);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlainVaultExportHtml() {
|
||||
initPlainVault();
|
||||
|
||||
openExportDialog();
|
||||
onView(withId(R.id.checkbox_export_encrypt)).perform(click());
|
||||
onView(withId(R.id.dropdown_export_format)).perform(click());
|
||||
onView(withText(R.string.export_format_html)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
onView(withId(R.id.checkbox_accept)).perform(click());
|
||||
File file = doExport();
|
||||
|
||||
checkHtmlExport(file);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncryptedVaultExportHtml() {
|
||||
initEncryptedVault();
|
||||
|
||||
openExportDialog();
|
||||
onView(withId(R.id.checkbox_export_encrypt)).perform(click());
|
||||
onView(withId(R.id.dropdown_export_format)).perform(click());
|
||||
onView(withText(R.string.export_format_html)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
onView(withId(R.id.checkbox_accept)).perform(click());
|
||||
File file = doExport();
|
||||
|
||||
checkHtmlExport(file);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSeparateExportPassword() {
|
||||
initEncryptedVault();
|
||||
setSeparateBackupExportPassword();
|
||||
|
||||
openExportDialog();
|
||||
File file = doExport();
|
||||
|
||||
readVault(file, VAULT_BACKUP_PASSWORD);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChangeBackupPassword() throws SlotIntegrityException {
|
||||
initEncryptedVault();
|
||||
setSeparateBackupExportPassword();
|
||||
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_section_security_title)), click()));
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_backup_password_change_title)), click()));
|
||||
onView(withId(R.id.text_password)).perform(typeText(VAULT_BACKUP_PASSWORD_CHANGED), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_BACKUP_PASSWORD_CHANGED), closeSoftKeyboard());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
onView(isRoot()).perform(pressBack());
|
||||
|
||||
VaultFileCredentials creds = _vaultManager.getVault().getCredentials();
|
||||
assertEquals(creds.getSlots().findRegularPasswordSlots().size(), 1);
|
||||
assertEquals(creds.getSlots().findBackupPasswordSlots().size(), 1);
|
||||
|
||||
for (PasswordSlot slot : creds.getSlots().findBackupPasswordSlots()) {
|
||||
verifyPasswordSlotChange(creds, slot, VAULT_BACKUP_PASSWORD, VAULT_BACKUP_PASSWORD_CHANGED);
|
||||
}
|
||||
|
||||
for (PasswordSlot slot : creds.getSlots().findRegularPasswordSlots()) {
|
||||
decryptPasswordSlot(slot, VAULT_PASSWORD);
|
||||
}
|
||||
|
||||
openExportDialog();
|
||||
File file = doExport();
|
||||
readVault(file, VAULT_BACKUP_PASSWORD_CHANGED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChangePasswordHavingBackupPassword() throws SlotIntegrityException {
|
||||
initEncryptedVault();
|
||||
setSeparateBackupExportPassword();
|
||||
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_section_security_title)), click()));
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_set_password_title)), click()));
|
||||
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD_CHANGED), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD_CHANGED), closeSoftKeyboard());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
onView(isRoot()).perform(pressBack());
|
||||
|
||||
VaultFileCredentials creds = _vaultManager.getVault().getCredentials();
|
||||
assertEquals(creds.getSlots().findRegularPasswordSlots().size(), 1);
|
||||
assertEquals(creds.getSlots().findBackupPasswordSlots().size(), 1);
|
||||
|
||||
for (PasswordSlot slot : creds.getSlots().findRegularPasswordSlots()) {
|
||||
verifyPasswordSlotChange(creds, slot, VAULT_PASSWORD, VAULT_PASSWORD_CHANGED);
|
||||
}
|
||||
|
||||
for (PasswordSlot slot : creds.getSlots().findBackupPasswordSlots()) {
|
||||
decryptPasswordSlot(slot, VAULT_BACKUP_PASSWORD);
|
||||
}
|
||||
|
||||
openExportDialog();
|
||||
File file = doExport();
|
||||
readVault(file, VAULT_BACKUP_PASSWORD);
|
||||
}
|
||||
|
||||
private void setSeparateBackupExportPassword() {
|
||||
VaultFileCredentials creds = _vaultManager.getVault().getCredentials();
|
||||
assertEquals(creds.getSlots().findRegularPasswordSlots().size(), 1);
|
||||
assertEquals(creds.getSlots().findBackupPasswordSlots().size(), 0);
|
||||
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_section_security_title)), click()));
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_backup_password_title)), click()));
|
||||
onView(withId(R.id.text_password)).perform(typeText(VAULT_BACKUP_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_BACKUP_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
onView(isRoot()).perform(pressBack());
|
||||
|
||||
creds = _vaultManager.getVault().getCredentials();
|
||||
assertEquals(creds.getSlots().findRegularPasswordSlots().size(), 1);
|
||||
assertEquals(creds.getSlots().findBackupPasswordSlots().size(), 1);
|
||||
for (PasswordSlot slot : creds.getSlots().findBackupPasswordSlots()) {
|
||||
verifyPasswordSlotChange(creds, slot, VAULT_PASSWORD, VAULT_BACKUP_PASSWORD);
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyPasswordSlotChange(VaultFileCredentials creds, PasswordSlot slot, String oldPassword, String newPassword) {
|
||||
assertThrows(SlotIntegrityException.class, () -> decryptPasswordSlot(slot, oldPassword));
|
||||
MasterKey masterKey;
|
||||
try {
|
||||
masterKey = decryptPasswordSlot(slot, newPassword);
|
||||
} catch (SlotIntegrityException e) {
|
||||
throw new RuntimeException("Unable to decrypt password slot", e);
|
||||
}
|
||||
|
||||
assertArrayEquals(creds.getKey().getBytes(), masterKey.getBytes());
|
||||
}
|
||||
|
||||
private File doExport() {
|
||||
File file = getExportFileUri();
|
||||
Intent resultData = new Intent();
|
||||
resultData.setData(Uri.fromFile(file));
|
||||
|
||||
Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData);
|
||||
intending(not(isInternal())).respondWith(result);
|
||||
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
return file;
|
||||
}
|
||||
|
||||
private void openExportDialog() {
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_section_import_export_title)), click()));
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_export_title)), click()));
|
||||
}
|
||||
|
||||
private MasterKey decryptPasswordSlot(PasswordSlot slot, String password) throws SlotIntegrityException {
|
||||
SecretKey derivedKey = slot.deriveKey(password.toCharArray());
|
||||
try {
|
||||
Cipher cipher = slot.createDecryptCipher(derivedKey);
|
||||
return slot.getKey(cipher);
|
||||
} catch (SlotException e) {
|
||||
throw new RuntimeException("Unable to decrypt password slot", e);
|
||||
}
|
||||
}
|
||||
|
||||
private File getExportFileUri() {
|
||||
String dirName = Hex.encode(CryptoUtils.generateRandomBytes(8));
|
||||
File dir = new File(getInstrumentation().getTargetContext().getExternalCacheDir(), String.format("export-%s", dirName));
|
||||
if (!dir.mkdirs()) {
|
||||
throw new RuntimeException(String.format("Unable to create export directory: %s", dir));
|
||||
}
|
||||
|
||||
VaultBackupManager.FileInfo fileInfo = new VaultBackupManager.FileInfo(VaultRepository.FILENAME_PREFIX_EXPORT);
|
||||
return new File(dir, fileInfo.toString());
|
||||
}
|
||||
|
||||
private VaultRepository readVault(File file, @Nullable String password) {
|
||||
VaultRepository repo;
|
||||
try (InputStream inStream = new FileInputStream(file)) {
|
||||
byte[] bytes = IOUtils.readAll(inStream);
|
||||
VaultFile vaultFile = VaultFile.fromBytes(bytes);
|
||||
|
||||
VaultFileCredentials creds = null;
|
||||
if (password != null) {
|
||||
SlotList slots = vaultFile.getHeader().getSlots();
|
||||
for (PasswordSlot slot : slots.findAll(PasswordSlot.class)) {
|
||||
SecretKey derivedKey = slot.deriveKey(password.toCharArray());
|
||||
Cipher cipher = slot.createDecryptCipher(derivedKey);
|
||||
MasterKey masterKey = slot.getKey(cipher);
|
||||
creds = new VaultFileCredentials(masterKey, slots);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
repo = VaultRepository.fromFile(getInstrumentation().getContext(), vaultFile, creds);
|
||||
} catch (SlotException | SlotIntegrityException | VaultRepositoryException | VaultFileException | IOException e) {
|
||||
throw new RuntimeException("Unable to read back vault file", e);
|
||||
}
|
||||
|
||||
checkReadEntries(repo.getEntries());
|
||||
return repo;
|
||||
}
|
||||
|
||||
private void readTxtExport(File file) {
|
||||
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(getInstrumentation().getContext());
|
||||
|
||||
Collection<VaultEntry> entries;
|
||||
try (InputStream inStream = new FileInputStream(file)) {
|
||||
DatabaseImporter.State state = importer.read(inStream);
|
||||
DatabaseImporter.Result result = state.convert();
|
||||
entries = result.getEntries().getValues();
|
||||
} catch (DatabaseImporterException | IOException e) {
|
||||
throw new RuntimeException("Unable to read txt export file", e);
|
||||
}
|
||||
|
||||
checkReadEntries(entries);
|
||||
}
|
||||
|
||||
private void checkHtmlExport(File file) {
|
||||
try (InputStream inStream = new FileInputStream(file)) {
|
||||
Reader inReader = new InputStreamReader(inStream, StandardCharsets.UTF_8);
|
||||
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
||||
XmlPullParser parser = factory.newPullParser();
|
||||
parser.setInput(inReader);
|
||||
while (parser.getEventType() != XmlPullParser.START_TAG) {
|
||||
parser.next();
|
||||
}
|
||||
if (!parser.getName().toLowerCase(Locale.ROOT).equals("html")) {
|
||||
throw new RuntimeException("not an html document!");
|
||||
}
|
||||
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
|
||||
parser.next();
|
||||
}
|
||||
} catch (IOException | XmlPullParserException e) {
|
||||
throw new RuntimeException("Unable to read html export file", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkReadEntries(Collection<VaultEntry> entries) {
|
||||
List<VaultEntry> vectors = VaultEntries.get();
|
||||
assertEquals(vectors.size(), entries.size());
|
||||
|
||||
int i = 0;
|
||||
for (VaultEntry entry : entries) {
|
||||
VaultEntry vector = vectors.get(i);
|
||||
String message = String.format("Entries are not equivalent: (%s) (%s)", vector.toJson().toString(), entry.toJson().toString());
|
||||
assertTrue(message, vector.equivalates(entry));
|
||||
try {
|
||||
assertEquals(message, vector.getInfo().getOtp(), entry.getInfo().getOtp());
|
||||
} catch (OtpInfoException e) {
|
||||
throw new RuntimeException("Unable to generate OTP", e);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@HiltAndroidTest
|
||||
@LargeTest
|
||||
public class DeepLinkTest extends AegisTest {
|
||||
@Before
|
||||
public void before() {
|
||||
initEmptyEncryptedVault();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeepLinkIntent() {
|
||||
VaultEntry entry = generateEntry(TotpInfo.class, "Bob", "Google");
|
||||
GoogleAuthInfo info = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
|
||||
launch(info.getUri());
|
||||
|
||||
onView(withId(R.id.action_save)).perform(click());
|
||||
|
||||
VaultEntry createdEntry = (VaultEntry) _vaultManager.getVault().getEntries().toArray()[0];
|
||||
assertTrue(createdEntry.equivalates(entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeepLinkIntent_Empty() {
|
||||
launch(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeepLinkIntent_Bad() {
|
||||
launch(Uri.parse("otpauth://bad"));
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void launch(Uri uri) {
|
||||
Intent intent = new Intent(getApp(), MainActivity.class);
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(uri);
|
||||
|
||||
// we need to use the deprecated ActivityTestRule class because of https://github.com/android/android-test/issues/143
|
||||
ActivityTestRule<MainActivity> rule = new ActivityTestRule<>(MainActivity.class);
|
||||
rule.launchActivity(intent);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
|
||||
import androidx.test.core.app.ActivityScenario;
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.rules.ScreenshotTestRule;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.RuleChain;
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@HiltAndroidTest
|
||||
@SmallTest
|
||||
public class EmptySecretTest extends AegisTest {
|
||||
private ActivityScenario<MainActivity> _scenario;
|
||||
|
||||
@Before
|
||||
public void before() throws OtpInfoException {
|
||||
initEmptyPlainVault();
|
||||
_vaultManager.getVault().addEntry(new VaultEntry(new TotpInfo(new byte[0])));
|
||||
|
||||
_scenario = ActivityScenario.launch(MainActivity.class);
|
||||
}
|
||||
|
||||
@After
|
||||
public void after() {
|
||||
_scenario.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVaultEntryEmptySecret() {
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.error_all_caps)), click()));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
|
||||
import static androidx.test.espresso.action.ViewActions.replaceText;
|
||||
import static androidx.test.espresso.action.ViewActions.typeText;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.intent.Intents.intending;
|
||||
import static androidx.test.espresso.intent.matcher.IntentMatchers.isInternal;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
|
||||
import static junit.framework.TestCase.assertFalse;
|
||||
import static junit.framework.TestCase.assertNull;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.test.espresso.IdlingRegistry;
|
||||
import androidx.test.espresso.IdlingResource;
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.espresso.intent.Intents;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.beemdevelopment.aegis.rules.ScreenshotTestRule;
|
||||
import com.beemdevelopment.aegis.ui.IntroActivity;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.RuleChain;
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@HiltAndroidTest
|
||||
@LargeTest
|
||||
public class IntroTest extends AegisTest {
|
||||
private final ActivityScenarioRule<IntroActivity> _activityRule = new ActivityScenarioRule<>(IntroActivity.class);
|
||||
|
||||
private ViewPager2IdlingResource _viewPager2IdlingResource;
|
||||
|
||||
@Rule
|
||||
public final TestRule testRule = RuleChain.outerRule(_activityRule).around(new ScreenshotTestRule());
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
Intents.init();
|
||||
|
||||
_activityRule.getScenario().onActivity(activity -> {
|
||||
_viewPager2IdlingResource = new ViewPager2IdlingResource(activity.findViewById(R.id.pager), "viewPagerIdlingResource");
|
||||
IdlingRegistry.getInstance().register(_viewPager2IdlingResource);
|
||||
});
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
Intents.release();
|
||||
IdlingRegistry.getInstance().unregister(_viewPager2IdlingResource);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntro_None() {
|
||||
assertFalse(_prefs.isIntroDone());
|
||||
ViewInteraction next = onView(withId(R.id.btnNext));
|
||||
ViewInteraction prev = onView(withId(R.id.btnPrevious));
|
||||
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
onView(withId(R.id.rb_none)).perform(click());
|
||||
prev.perform(click());
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
|
||||
VaultRepository vault = _vaultManager.getVault();
|
||||
assertFalse(vault.isEncryptionEnabled());
|
||||
assertNull(vault.getCredentials());
|
||||
assertTrue(_prefs.isIntroDone());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntro_Password() {
|
||||
assertFalse(_prefs.isIntroDone());
|
||||
ViewInteraction next = onView(withId(R.id.btnNext));
|
||||
ViewInteraction prev = onView(withId(R.id.btnPrevious));
|
||||
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
onView(withId(R.id.rb_password)).perform(click());
|
||||
prev.perform(click());
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD + "1"), closeSoftKeyboard());
|
||||
next.perform(click());
|
||||
onView(withId(R.id.text_password_confirm)).perform(replaceText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
prev.perform(click());
|
||||
prev.perform(click());
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
|
||||
VaultRepository vault = _vaultManager.getVault();
|
||||
SlotList slots = vault.getCredentials().getSlots();
|
||||
assertTrue(vault.isEncryptionEnabled());
|
||||
assertTrue(slots.has(PasswordSlot.class));
|
||||
assertFalse(slots.has(BiometricSlot.class));
|
||||
assertTrue(_prefs.isIntroDone());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntro_Import_Plain() {
|
||||
assertFalse(_prefs.isIntroDone());
|
||||
Uri uri = getResourceUri("aegis_plain.json");
|
||||
Intent resultData = new Intent();
|
||||
resultData.setData(uri);
|
||||
|
||||
Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData);
|
||||
intending(not(isInternal())).respondWith(result);
|
||||
|
||||
ViewInteraction next = onView(withId(R.id.btnNext));
|
||||
onView(withId(R.id.btnImport)).perform(click());
|
||||
next.perform(click());
|
||||
|
||||
VaultRepository vault = _vaultManager.getVault();
|
||||
assertFalse(vault.isEncryptionEnabled());
|
||||
assertNull(vault.getCredentials());
|
||||
assertTrue(_prefs.isIntroDone());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntro_Import_Encrypted() {
|
||||
assertFalse(_prefs.isIntroDone());
|
||||
Uri uri = getResourceUri("aegis_encrypted.json");
|
||||
Intent resultData = new Intent();
|
||||
resultData.setData(uri);
|
||||
|
||||
Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData);
|
||||
intending(not(isInternal())).respondWith(result);
|
||||
|
||||
ViewInteraction next = onView(withId(R.id.btnNext));
|
||||
onView(withId(R.id.btnImport)).perform(click());
|
||||
onView(withId(R.id.text_input)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
next.perform(click());
|
||||
|
||||
VaultRepository vault = _vaultManager.getVault();
|
||||
SlotList slots = vault.getCredentials().getSlots();
|
||||
assertTrue(vault.isEncryptionEnabled());
|
||||
assertTrue(slots.has(PasswordSlot.class));
|
||||
assertFalse(slots.has(BiometricSlot.class));
|
||||
assertTrue(_prefs.isIntroDone());
|
||||
}
|
||||
|
||||
private Uri getResourceUri(String resourceName) {
|
||||
File targetFile = new File(getInstrumentation().getTargetContext().getExternalCacheDir(), resourceName);
|
||||
try (InputStream inStream = getClass().getResourceAsStream(resourceName);
|
||||
FileOutputStream outStream = new FileOutputStream(targetFile)) {
|
||||
IOUtils.copy(inStream, outStream);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return Uri.fromFile(targetFile);
|
||||
}
|
||||
|
||||
// Source: https://stackoverflow.com/a/32763454/12972657
|
||||
private static class ViewPager2IdlingResource implements IdlingResource {
|
||||
private final String _resName;
|
||||
private boolean _isIdle = true;
|
||||
private IdlingResource.ResourceCallback _resourceCallback = null;
|
||||
|
||||
public ViewPager2IdlingResource(ViewPager2 viewPager, String resName) {
|
||||
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {
|
||||
_isIdle = (state == ViewPager2.SCROLL_STATE_IDLE || state == ViewPager2.SCROLL_STATE_DRAGGING);
|
||||
if (_isIdle && _resourceCallback != null) {
|
||||
_resourceCallback.onTransitionToIdle();
|
||||
}
|
||||
}
|
||||
});
|
||||
_resName = resName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return _resName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIdleNow() {
|
||||
return _isIdle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerIdleTransitionCallback(IdlingResource.ResourceCallback resourceCallback) {
|
||||
_resourceCallback = resourceCallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu;
|
||||
import static androidx.test.espresso.action.ViewActions.clearText;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
|
||||
import static androidx.test.espresso.action.ViewActions.longClick;
|
||||
import static androidx.test.espresso.action.ViewActions.pressBack;
|
||||
import static androidx.test.espresso.action.ViewActions.scrollTo;
|
||||
import static androidx.test.espresso.action.ViewActions.typeText;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static junit.framework.TestCase.assertFalse;
|
||||
import static junit.framework.TestCase.assertNull;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions;
|
||||
import androidx.test.espresso.matcher.RootMatchers;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.MotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.YandexInfo;
|
||||
import com.beemdevelopment.aegis.rules.ScreenshotTestRule;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.ui.views.EntryAdapter;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.RuleChain;
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@HiltAndroidTest
|
||||
@LargeTest
|
||||
public class OverallTest extends AegisTest {
|
||||
private static final String _groupName = "Test";
|
||||
|
||||
private final ActivityScenarioRule<MainActivity> _activityRule = new ActivityScenarioRule<>(MainActivity.class);
|
||||
|
||||
@Rule
|
||||
public final TestRule testRule = RuleChain.outerRule(_activityRule).around(new ScreenshotTestRule());
|
||||
|
||||
@Test
|
||||
public void testOverall() {
|
||||
ViewInteraction next = onView(withId(R.id.btnNext));
|
||||
next.perform(click());
|
||||
onView(withId(R.id.rb_password)).perform(click());
|
||||
next.perform(click());
|
||||
onView(withId(R.id.text_password)).perform(click()).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
next.perform(click());
|
||||
onView(withId(R.id.btnNext)).perform(click());
|
||||
|
||||
VaultRepository vault = _vaultManager.getVault();
|
||||
assertTrue(vault.isEncryptionEnabled());
|
||||
assertTrue(vault.getCredentials().getSlots().has(PasswordSlot.class));
|
||||
assertTrue(_prefs.isIntroDone());
|
||||
|
||||
List<VaultEntry> entries = Arrays.asList(
|
||||
generateEntry(TotpInfo.class, "Frank", "Google"),
|
||||
generateEntry(HotpInfo.class, "John", "GitHub"),
|
||||
generateEntry(TotpInfo.class, "Alice", "Office 365"),
|
||||
generateEntry(SteamInfo.class, "Gaben", "Steam"),
|
||||
generateEntry(YandexInfo.class, "Ivan", "Yandex", 16),
|
||||
generateEntry(MotpInfo.class, "Jimmy McGill", "PfSense", 16)
|
||||
);
|
||||
for (VaultEntry entry : entries) {
|
||||
addEntry(entry);
|
||||
}
|
||||
|
||||
List<VaultEntry> realEntries = new ArrayList<>(vault.getEntries());
|
||||
for (int i = 0; i < realEntries.size(); i++) {
|
||||
String message = String.format("%s != %s", realEntries.get(i).toJson().toString(), entries.get(i).toJson().toString());
|
||||
assertTrue(message, realEntries.get(i).equivalates(entries.get(i)));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnHolderItem(withOtpType(HotpInfo.class), clickChildViewWithId(R.id.buttonRefresh)));
|
||||
}
|
||||
|
||||
AtomicBoolean isErrorCardShown = new AtomicBoolean(false);
|
||||
_activityRule.getScenario().onActivity(activity -> {
|
||||
isErrorCardShown.set(((EntryAdapter)((RecyclerView) activity.findViewById(R.id.rvKeyProfiles)).getAdapter()).isErrorCardShown());
|
||||
});
|
||||
|
||||
int entryPosOffset = isErrorCardShown.get() ? 1 : 0;
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 0, longClick()));
|
||||
onView(withId(R.id.action_copy)).perform(click());
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 1, longClick()));
|
||||
onView(withId(R.id.action_edit)).perform(click());
|
||||
onView(withId(R.id.text_name)).perform(clearText(), typeText("Bob"), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_group)).perform(click());
|
||||
onView(withId(R.id.addGroup)).inRoot(RootMatchers.isDialog()).perform(click());
|
||||
onView(withId(R.id.text_input)).perform(typeText(_groupName), closeSoftKeyboard());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
onView(withText(R.string.save)).perform(click());
|
||||
onView(isRoot()).perform(pressBack());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
changeSort(R.string.sort_alphabetically_name);
|
||||
changeSort(R.string.sort_alphabetically_name_reverse);
|
||||
changeSort(R.string.sort_alphabetically);
|
||||
changeSort(R.string.sort_alphabetically_reverse);
|
||||
changeSort(R.string.sort_custom);
|
||||
|
||||
changeGroupFilter(_groupName);
|
||||
changeGroupFilter(null);
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 2, longClick()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 3, click()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 4, click()));
|
||||
onView(withId(R.id.action_share_qr)).perform(click());
|
||||
onView(withId(R.id.btnNext)).perform(click()).perform(click()).perform(click());
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 0, longClick()));
|
||||
onView(allOf(isDescendantOfA(withClassName(containsString("ActionBarContextView"))), withClassName(containsString("OverflowMenuButton")))).perform(click());
|
||||
onView(withText(R.string.action_delete)).perform(click());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
openContextualActionModeOverflowMenu();
|
||||
onView(withText(R.string.lock)).perform(click());
|
||||
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(R.id.button_decrypt)).perform(click());
|
||||
vault = _vaultManager.getVault();
|
||||
|
||||
openContextualActionModeOverflowMenu();
|
||||
onView(withText(R.string.action_settings)).perform(click());
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_section_security_title)), click()));
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItemAtPosition(1, click()));
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
assertFalse(vault.isEncryptionEnabled());
|
||||
assertNull(vault.getCredentials());
|
||||
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItemAtPosition(1, click()));
|
||||
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
assertTrue(vault.isEncryptionEnabled());
|
||||
assertTrue(vault.getCredentials().getSlots().has(PasswordSlot.class));
|
||||
}
|
||||
|
||||
private void changeSort(@IdRes int resId) {
|
||||
onView(withId(R.id.action_sort)).perform(click());
|
||||
onView(withText(resId)).perform(click());
|
||||
}
|
||||
|
||||
private void changeGroupFilter(String text) {
|
||||
if (text == null) {
|
||||
onView(allOf(withText(R.string.no_group), isDescendantOfA(withId(R.id.groupChipGroup)))).perform(click());
|
||||
} else {
|
||||
onView(allOf(withText(text), isDescendantOfA(withId(R.id.groupChipGroup)))).perform(click());
|
||||
}
|
||||
}
|
||||
|
||||
private void addEntry(VaultEntry entry) {
|
||||
onView(withId(R.id.fab)).perform(click());
|
||||
onView(withId(R.id.fab_enter)).perform(click());
|
||||
|
||||
onView(withId(R.id.accordian_header)).perform(scrollTo(), click());
|
||||
onView(withId(R.id.text_name)).perform(typeText(entry.getName()), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_issuer)).perform(typeText(entry.getIssuer()), closeSoftKeyboard());
|
||||
|
||||
if (entry.getInfo().getClass() != TotpInfo.class) {
|
||||
String otpType;
|
||||
if (entry.getInfo() instanceof HotpInfo) {
|
||||
otpType = "HOTP";
|
||||
} else if (entry.getInfo() instanceof SteamInfo) {
|
||||
otpType = "Steam";
|
||||
} else if (entry.getInfo() instanceof YandexInfo) {
|
||||
otpType = "Yandex";
|
||||
} else if (entry.getInfo() instanceof MotpInfo) {
|
||||
otpType = "MOTP";
|
||||
} else if (entry.getInfo() instanceof TotpInfo) {
|
||||
otpType = "TOTP";
|
||||
} else {
|
||||
throw new RuntimeException(String.format("Unexpected entry type: %s", entry.getInfo().getClass().getSimpleName()));
|
||||
}
|
||||
|
||||
onView(withId(R.id.dropdown_type)).perform(scrollTo(), click());
|
||||
onView(withText(otpType)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
|
||||
}
|
||||
|
||||
String secret;
|
||||
if (Objects.equals(entry.getInfo().getTypeId(), MotpInfo.ID)) {
|
||||
secret = Hex.encode(entry.getInfo().getSecret());
|
||||
} else {
|
||||
secret = Base32.encode(entry.getInfo().getSecret());
|
||||
}
|
||||
|
||||
onView(withId(R.id.text_secret)).perform(typeText(secret), closeSoftKeyboard());
|
||||
|
||||
if (entry.getInfo() instanceof YandexInfo) {
|
||||
String pin = "123456";
|
||||
((YandexInfo) entry.getInfo()).setPin(pin);
|
||||
onView(withId(R.id.text_pin)).perform(typeText(pin), closeSoftKeyboard());
|
||||
} else if (entry.getInfo() instanceof MotpInfo) {
|
||||
String pin = "1234";
|
||||
((MotpInfo) entry.getInfo()).setPin(pin);
|
||||
onView(withId(R.id.text_pin)).perform(typeText(pin), closeSoftKeyboard());
|
||||
}
|
||||
|
||||
onView(withId(R.id.action_save)).perform(click());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.SmallTest;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import com.beemdevelopment.aegis.ui.PanicResponderActivity;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@HiltAndroidTest
|
||||
@SmallTest
|
||||
public class PanicTriggerTest extends AegisTest {
|
||||
@Before
|
||||
public void before() {
|
||||
initEncryptedVault();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPanicTriggerDisabled() {
|
||||
assertFalse(_prefs.isPanicTriggerEnabled());
|
||||
assertTrue(_vaultManager.isVaultLoaded());
|
||||
launchPanic();
|
||||
assertTrue(_vaultManager.isVaultLoaded());
|
||||
_vaultManager.getVault();
|
||||
assertTrue(VaultRepository.fileExists(getApp()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPanicTriggerEnabled() {
|
||||
_prefs.setIsPanicTriggerEnabled(true);
|
||||
assertTrue(_prefs.isPanicTriggerEnabled());
|
||||
assertTrue(_vaultManager.isVaultLoaded());
|
||||
launchPanic();
|
||||
assertFalse(_vaultManager.isVaultLoaded());
|
||||
assertThrows(IllegalStateException.class, () -> _vaultManager.getVault());
|
||||
assertFalse(VaultRepository.fileExists(getApp()));
|
||||
}
|
||||
|
||||
private void launchPanic() {
|
||||
Intent intent = new Intent(PanicResponderActivity.PANIC_TRIGGER_ACTION);
|
||||
// we need to use the deprecated ActivityTestRule class because of https://github.com/android/android-test/issues/143
|
||||
ActivityTestRule<PanicResponderActivity> rule = new ActivityTestRule<>(PanicResponderActivity.class);
|
||||
rule.launchActivity(intent);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.beemdevelopment.aegis.rules;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import androidx.test.runner.screenshot.BasicScreenCaptureProcessor;
|
||||
import androidx.test.runner.screenshot.ScreenCapture;
|
||||
import androidx.test.runner.screenshot.ScreenCaptureProcessor;
|
||||
import androidx.test.runner.screenshot.Screenshot;
|
||||
|
||||
import org.junit.rules.TestWatcher;
|
||||
import org.junit.runner.Description;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
|
||||
public class ScreenshotTestRule extends TestWatcher {
|
||||
@Override
|
||||
protected void failed(Throwable e, Description description) {
|
||||
super.failed(e, description);
|
||||
|
||||
String filename = description.getTestClass().getSimpleName() + "-" + description.getMethodName();
|
||||
|
||||
ScreenCapture capture = Screenshot.capture();
|
||||
capture.setName(filename);
|
||||
capture.setFormat(Bitmap.CompressFormat.PNG);
|
||||
|
||||
HashSet<ScreenCaptureProcessor> processors = new HashSet<>();
|
||||
processors.add(new BasicScreenCaptureProcessor());
|
||||
|
||||
try {
|
||||
capture.process(processors);
|
||||
} catch (IOException e2) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisTest;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@HiltAndroidTest
|
||||
@SmallTest
|
||||
public class VaultRepositoryTest extends AegisTest {
|
||||
@Before
|
||||
public void before() {
|
||||
initEncryptedVault();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToggleEncryption() throws VaultRepositoryException {
|
||||
VaultRepository vault = _vaultManager.getVault();
|
||||
_vaultManager.disableEncryption();
|
||||
assertFalse(vault.isEncryptionEnabled());
|
||||
assertNull(vault.getCredentials());
|
||||
|
||||
VaultFileCredentials creds = generateCredentials();
|
||||
_vaultManager.enableEncryption(creds);
|
||||
assertTrue(vault.isEncryptionEnabled());
|
||||
assertNotNull(vault.getCredentials());
|
||||
assertEquals(vault.getCredentials().getSlots().findAll(PasswordSlot.class).size(), 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../../../../../test/java/com/beemdevelopment/aegis/vectors/VaultEntries.java
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../../../../test/resources/com/beemdevelopment/aegis/importers/aegis_encrypted.json
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../../../../test/resources/com/beemdevelopment/aegis/importers/aegis_plain.json
|
||||
166
app/src/main/AndroidManifest.xml
Normal file
166
app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<!-- NOTE: Disabled for now. See issue: #1047
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
-->
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera.autofocus"
|
||||
android:required="false" />
|
||||
|
||||
<application
|
||||
android:name=".AegisApplication"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupOnly="true"
|
||||
android:fullBackupContent="@xml/backup_rules_old"
|
||||
android:dataExtractionRules="@xml/backup_rules"
|
||||
android:backupAgent=".AegisBackupAgent"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:icon="@mipmap/${iconName}"
|
||||
android:label="Aegis"
|
||||
android:supportsRtl="true"
|
||||
android:largeHeap="true"
|
||||
android:theme="@style/Theme.Aegis.Launch"
|
||||
tools:targetApi="tiramisu">
|
||||
<activity android:name=".ui.TransferEntriesActivity"
|
||||
android:label="@string/title_activity_transfer" />
|
||||
<activity
|
||||
android:name=".ui.AboutActivity"
|
||||
android:label="@string/title_activity_about" />
|
||||
<activity
|
||||
android:name=".ui.ImportEntriesActivity"
|
||||
android:label="@string/title_activity_import_entries" />
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
android:label="${title}">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="otpauth" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.ScannerActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:label="@string/title_activity_scan_qr"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".ui.EditEntryActivity"
|
||||
android:label="@string/title_activity_edit_entry" />
|
||||
<activity
|
||||
android:name=".ui.IntroActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".ui.AuthActivity" />
|
||||
<activity
|
||||
android:name=".ui.PreferencesActivity"
|
||||
android:label="@string/title_activity_preferences" />
|
||||
<activity
|
||||
android:name=".ui.GroupManagerActivity"
|
||||
android:label="@string/title_activity_manage_groups" />
|
||||
<activity android:name=".ui.AssignIconsActivity"
|
||||
android:label="@string/title_activity_assign_icons"/>
|
||||
<activity android:name=".ui.LicensesActivity"
|
||||
android:label="@string/title_activity_licenses"/>
|
||||
<activity
|
||||
android:name=".ui.PanicResponderActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:noHistory="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="info.guardianproject.panic.action.TRIGGER" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".ui.ExitActivity" />
|
||||
|
||||
<!-- NOTE: Disabled for now. See issue: #1047
|
||||
<service android:name=".services.NotificationService" />
|
||||
-->
|
||||
|
||||
<service
|
||||
android:name=".services.LaunchAppTileService"
|
||||
android:label="@string/tile_open_vault"
|
||||
android:icon="@drawable/ic_aegis_quicksettings"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".services.LaunchScannerTileService"
|
||||
android:label="@string/tile_open_scanner"
|
||||
android:icon="@drawable/ic_aegis_quicksettings"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<receiver android:name=".receivers.VaultLockReceiver" android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="${applicationId}.LOCK_VAULT" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${fileProviderAuthority}"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths">
|
||||
</meta-data>
|
||||
</provider>
|
||||
|
||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut" android:value="true" />
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
<package android:name="com.stratumauth.app" />
|
||||
<package android:name="com.authy.authy" />
|
||||
<package android:name="org.fedorahosted.freeotp" />
|
||||
<package android:name="org.liberty.android.freeotpplus" />
|
||||
<package android:name="com.google.android.apps.authenticator2" />
|
||||
<package android:name="com.azure.authenticator" />
|
||||
<package android:name="com.valvesoftware.android.steam.community" />
|
||||
<package android:name="com.authenticator.authservice2" />
|
||||
<package android:name="com.duosecurity.duomobile" />
|
||||
<package android:name="com.blizzard.messenger" />
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
1
app/src/main/assets/LICENSE
Symbolic link
1
app/src/main/assets/LICENSE
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../../LICENSE
|
||||
549
app/src/main/assets/changelog.html
Normal file
549
app/src/main/assets/changelog.html
Normal file
|
|
@ -0,0 +1,549 @@
|
|||
<html>
|
||||
<head>
|
||||
<style type="text/css">
|
||||
* {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
body {
|
||||
background-color: %1$s;
|
||||
color: %2$s;
|
||||
}
|
||||
ul {
|
||||
list-style-position: inside;
|
||||
padding: 0;
|
||||
padding-left: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
li {
|
||||
padding-bottom: 8px;
|
||||
list-style-position: outside;
|
||||
margin-left: 1em;
|
||||
}
|
||||
h4 {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
h3 {
|
||||
margin-bottom: 7px;
|
||||
padding-top: 7px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div></div>
|
||||
<h3>Version 3.4.1</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Support for importing from Proton Authenticator</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>The autofill service would show a prompt to save the PIN as a password</li>
|
||||
</ul>
|
||||
<h3>Version 3.4</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Haptic feedback when an entry is about to expire</li>
|
||||
<li>Brightness increase is now toggleable in the entry transfer view</li>
|
||||
<li>Filter on multiple groups simultaneously</li>
|
||||
<li>Color contrast on hidden codes has been improved</li>
|
||||
<li>Prompt before the user is about to save an entry with a duplicate name/issuer combination</li>
|
||||
<li>New languages: Estonian, Korean, Malayalam, Norwegian (Bokmål) and Serbian</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>A crash could occur if an entry with period 7 exists and code expiry indication is enabled</li>
|
||||
<li>The Portuguese (Brazilian) locale was used even if Portuguese was configured</li>
|
||||
<li>FreeOTP import would fail if the algorithm or digits field was not specified for an entry</li>
|
||||
<li>The divider between entries would be missing in certain filter configurations</li>
|
||||
<li>The snackbar in try entry importing view could obstruct the name of an entry</li>
|
||||
</ul>
|
||||
<h4>Miscellaneous</h4>
|
||||
<ul>
|
||||
<li>Android 6 or newer is now required the run the app</li>
|
||||
</ul>
|
||||
<h3>Version 3.3.4</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Icons are now resized to 512x512 to reduce the size of the vault file and to reduce the chance of encountering out of memory conditions</li>
|
||||
</ul>
|
||||
<h3>Version 3.3.3</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Some users ran into out of memory conditions due to large icons in their vault file. We've introduced a temporary measure that should help in most cases, but we'll follow up with a more comprehensive fix soon.</li>
|
||||
<li>Window insets were not always applied correctly, causing parts of the UI to appear off-screen</li>
|
||||
<li>The 2FAS importer did not tolerate spaces for secrets and was not always able to extract the issuer</li>
|
||||
</ul>
|
||||
<h3>Version 3.3.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Find entries by searching in multiple fields simultaneously</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Entries would not actually be added to the Aegis vault in some cases when importing from Google Authenticator export QR codes</li>
|
||||
<li>The lock button was sometimes shown for unencrypted vaults</li>
|
||||
<li>The sort category menu item did not always reflect the current sorting</li>
|
||||
<li>The next code was not always easy to read because its color had low contrast with the background</li>
|
||||
<li>Entry selection was not cancelled when changing the group filter</li>
|
||||
</ul>
|
||||
<h3>Version 3.3.1</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Codes were not shown in case the tiles view mode was combined with hidden account names</li>
|
||||
</ul>
|
||||
<h3>Version 3.3</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Significant improvements to group filtering
|
||||
<ul>
|
||||
<li>Groups can now be filtered on straight from the main view instead of through a dialog</li>
|
||||
<li>Ability to assign multiple entries to a group in one go</li>
|
||||
<li>Support for reordering groups</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Codes now change color when they're about to expire</li>
|
||||
<li>Option to show the next code ahead of time</li>
|
||||
<li>Support for backing up to a single file (This enables support for more cloud providers, such as Google Drive)</li>
|
||||
<li>Various minor improvements to make QR code exports easier to scan</li>
|
||||
<li>Support for importing from Ente Auth</li>
|
||||
<li>Support for importing FreeOTP 2 backups</li>
|
||||
<li>Updated translations</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>QR codes exported for Google Authenticator could not be scanned on iOS</li>
|
||||
<li>The code would be copied after a single tap in case "Tap to reveal" and "Copy tokens to the clipboard" were enabled simultaneously</li>
|
||||
<li>Various other minor UI, stability and performance improvements</li>
|
||||
</ul>
|
||||
<h3>Version 3.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>The ability to add a single entry to multiple groups</li>
|
||||
<li>Option to keep an infinite number of backups</li>
|
||||
<li>Option to customize which fields to search for in entries</li>
|
||||
<li>Allow hiding entry names in the tiled view mode</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>With "Tap to reveal" enabled, the size of the shown dots would not be consistent with the size of the code digits, on some devices</li>
|
||||
<li>After importing a backup, the UI would in some cases incorrectly claim that biometric unlock is enabled</li>
|
||||
<li>The export dialog was not fully visible on some devices</li>
|
||||
<li>Various other minor UI, stability and performance improvements</li>
|
||||
</ul>
|
||||
<h3>Version 3.1.1</h3>
|
||||
<h4>Fixes</h4>
|
||||
<p>
|
||||
A recent Android Pixel update introduced a bug causing Aegis to sometimes show a black screen after unlocking the vault.
|
||||
We have reported this issue to the Google Issue Tracker (<a href="https://issuetracker.google.com/issues/352963108">link</a>) and
|
||||
are awaiting a response from Google. In the meantime, we have implemented a workaround that eliminates this bug.
|
||||
</p>
|
||||
<ul>
|
||||
<li>Group filter now gets applied properly upon unlocking the vault</li>
|
||||
<li>Advanced entry settings now gets shown correctly</li>
|
||||
<li>Keyboard when searching for entries now gets hidden when the user starts scrolling through the list</li>
|
||||
</ul>
|
||||
<h3>Version 3.1</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>A new audit log has been added to check all important events that occurred in your vault</li>
|
||||
<li>Added the ability to rename groups</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Group selection will now be remembered again upon launch</li>
|
||||
<li>Various UI improvements</li>
|
||||
<li>Stability fixes</li>
|
||||
</ul>
|
||||
<h3>Version 3.0.1</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Support for importing from the new Battle.net app</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Visual glitches when AMOLED theme was used on old Android versions</li>
|
||||
<li>Minor UI improvements</li>
|
||||
</ul>
|
||||
<h3>Version 3.0</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Material 3 (and Material You)</li>
|
||||
<li>Automatic assignment of icons to entries</li>
|
||||
<li>Ability to select all entries in one go</li>
|
||||
<li>Support for importing 2FAS schema v4 backups</li>
|
||||
<li>Sort entries based on the last time they were used</li>
|
||||
<li>Some clarifications related to importing and backup permission errors</li>
|
||||
<li>Preparations for the ability to assign a single entry to multiple groups</li>
|
||||
<li>Performance improvements when scrolling through an entry list with lots of icons</li>
|
||||
<li>A new look for the third-party licenses list</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Directly importing from Authy using root would fail</li>
|
||||
<li>Minor glitches related to animation duration scale settings</li>
|
||||
<li>Various stability improvements</li>
|
||||
</ul>
|
||||
<h3>Version 2.2.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>An optional name field for icon packs to bypass filename character restrictions</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>The Authenticator Pro importer only supported the legacy backup format</li>
|
||||
<li>A crash could occur in the tile service</li>
|
||||
</ul>
|
||||
<h3>Version 2.2.1</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Ability to automatically skip potential duplicates when importing entries</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Biometrics button on the unlock screen was unresponsive</li>
|
||||
</ul>
|
||||
<h3>Version 2.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Authenticator Pro encrypted import support</li>
|
||||
<li>Ability to change account name position</li>
|
||||
<li>A new dialog explaining how our password reminder works</li>
|
||||
<li>Ability to change copy behavior</li>
|
||||
<li>Ability to only show account names when necessary</li>
|
||||
<li>New view mode: Tiles/Grid</li>
|
||||
<li>Added translation: Dutch (Frysian)</li>
|
||||
<li>Updated translations</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Deleting an entry while a search filter is active now shows the correct state</li>
|
||||
<li>Aegis now fully respects system animation settings</li>
|
||||
</ul>
|
||||
<h3>Version 2.1.3</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Option to disable the backup reminder</li>
|
||||
<li>Improved group selection dropdown during vault export</li>
|
||||
<li>New translation: Hebrew</li>
|
||||
<li>Updated translations</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>A crash could occur because a Toast was incorrectly created</li>
|
||||
</ul>
|
||||
<h3>Version 2.1.2</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>A crash could occur when changing an entry in such a way that it is filtered out from the entry list</li>
|
||||
</ul>
|
||||
<h3>Version 2.1.1</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>An option to export the vault as an HTML file</li>
|
||||
<li>Support for importing from Battle.net Authenticator (root required)</li>
|
||||
<li>An option to hide entry icons</li>
|
||||
<li>An option to only include certain groups in an export</li>
|
||||
<li>Copying a token now takes a second tap if tap to reveal is enabled</li>
|
||||
<li>The ability to copy the URI when transferring entries through QR codes</li>
|
||||
<li>Updated translations</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>The lock notification would remain after locking the vault in certain cases. For now, we've disabled the notification entirely.</li>
|
||||
<li>Making changes to an entry while having one or more favorited entries in the vault could result in buggy ordering</li>
|
||||
<li>Tapping to the reveal a token could increase the height of the entry in certain view modes on recent Android versions</li>
|
||||
<li>The backup reminder was unclear about when the last successful backup took place</li>
|
||||
<li>Users could accidentally select MD5 as the hash algorithm for non-mOTP entry types, causing crashes at seemingly random intervals. Any users who have gotten themselves into this situation will see these bad entries get reset to SHA1.</li>
|
||||
<li>Importing from certain apps would cause a crash if an empty password was entered</li>
|
||||
<li>The andOTP importer could hang indefinitely if the user accidentally selected a non-andOTP file.</li>
|
||||
<li>Various other stability improvements</li>
|
||||
</ul>
|
||||
<h3>Version 2.1</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Support for mOTP</li>
|
||||
<li>Support for Yandex OTP (Experimental)</li>
|
||||
<li>An Adaptive Icon for Material You</li>
|
||||
<li>Ability to favorite certain entries and pin them to the top of the entry list</li>
|
||||
<li>Ability to filter by entries that are not in a group</li>
|
||||
<li>Ability to set a separate password that is used for encrypting backups and exports</li>
|
||||
<li>Support for predictive back gesture</li>
|
||||
<li>Improved overview of backup status in preferences</li>
|
||||
<li>Additional options for code digit grouping</li>
|
||||
<li>Support for importing from Duo</li>
|
||||
<li>Support for importing from Bitwarden</li>
|
||||
<li>Support for importing multiple QR code images in one go</li>
|
||||
<li>Support for scanning Google Authenticator export QR codes from image files</li>
|
||||
<li>Display some extra information in the dialog displayed when deleting an entry</li>
|
||||
<li>An option to export through Google Authenticator export QR code images</li>
|
||||
<li>An option to import an existing vault file from the first page in the intro</li>
|
||||
<li>An option to minimize the app after copying a token</li>
|
||||
<li>A count of the total number of entries is displayed at the bottom of the entry list</li>
|
||||
<li>A backup reminder is shown if changes were made to the vault, but no backup or export has been created yet since then</li>
|
||||
<li>A warning is shown after a plaintext export has been made</li>
|
||||
<li>An option to focus search immediately after the app starts</li>
|
||||
<li>Allow customization of the frequency of the password reminder</li>
|
||||
<li>Allow sharing text to Aegis in the format of a Google Authenticator URI to add as a new entry</li>
|
||||
<li>Always allow D2D (device-to-device) Android backups regardless of backup settings</li>
|
||||
<li>Mark clipboard data as sensitive when copying tokens so that Android will mask them in the UI</li>
|
||||
<li>Updated translations for almost all languages</li>
|
||||
<li>New languages: Asturian, Catalan, Galician</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Various reliability improvements for the QR code scanner</li>
|
||||
<li>The floating action button was glitchy when making small entry list scroll movements</li>
|
||||
<li>The vault unlocked notification was never shown and was still using the old app icon</li>
|
||||
<li>The automatically generated entry icon was broken if the entry name/issuer is a multi-codepoint character (certain emoji's, for example)</li>
|
||||
<li>The PIN keyboard was not disabled after enabling encryption</li>
|
||||
<li>The password prompt message was unclear when importing from a file</li>
|
||||
<li>The entry list was not sorted correctly if a change to an entry caused its location to change</li>
|
||||
<li>Quickly double-tapping on the copy button would cause a crash</li>
|
||||
<li>Importing an entry with an empty secret would cause a crash loop</li>
|
||||
<li>On certain devices, it was not possible to import icon packs because the .ZIP files would be grayed out</li>
|
||||
<li>An unclear error message was shown when trying to import from Steam and Google Authenticator</li>
|
||||
<li>Various other minor UI and stability improvements</li>
|
||||
</ul>
|
||||
<h3>Version 2.0.3</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Support for importing 2FAS Authenticator's new backup format</li>
|
||||
</ul>
|
||||
<h3>Version 2.0.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Add a note field to entries</li>
|
||||
<li>An option to pause code updating of highlighted entries</li>
|
||||
<li>New translation: Lithuanian</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Minor UI and stability improvements</li>
|
||||
<li>The Microsoft Authenticator importer did not accept spaces and dashes in secrets</li>
|
||||
</ul>
|
||||
<h3>Version 2.0.1</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Support for sorting on most used tokens</li>
|
||||
<li>Some minor UX and stability improvements</li>
|
||||
<li>New translation: Vietnamese</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>QR code information was decoded incorrectly in some cases if the app was set to a certain language (Turkish, for example)</li>
|
||||
</ul>
|
||||
<h3>Version 2.0</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Support for icon packs</li>
|
||||
<li>Support for participation in Android's backup system (Google Drive, Seedvault)</li>
|
||||
<li>UI refresh (switched to the Material Components theme)</li>
|
||||
<li>Bottom sheet with chips to filter on groups</li>
|
||||
<li>Support for importing from 2FAS Authenticator</li>
|
||||
<li>Search in account names by default (and remove the setting)</li>
|
||||
<li>Replaced the FAB with a bottom sheet dialog</li>
|
||||
<li>Reorganization of settings into separate categories</li>
|
||||
<li>Ability to 'share' images of QR codes to scan in Aegis</li>
|
||||
<li>Option to save the current group filter</li>
|
||||
<li>New translations for Bulgarian, Danish, Latvian, Swedish and Ukranian</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>The QR code scanner had trouble detecting QR codes on some devices due to low resolution image capture</li>
|
||||
<li>The app would vanish from the recent apps list after locking</li>
|
||||
<li>When importing from Nextcloud, Aegis would report that the file could not be found.</li>
|
||||
<li>The biometrics prompt would not appear on some devices</li>
|
||||
<li>The app would lock when selecting a file/icon on certain devices and configurations</li>
|
||||
<li>There were multiple layout issues on small screen devices</li>
|
||||
<li>Various other usability, performance and stability improvements</li>
|
||||
</ul>
|
||||
<h3>Version 1.4.2</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>The app would crash if DocumentsUI is not present on the device</li>
|
||||
<li>The app would close when selecting an icon if auto lock on minimize was enabled</li>
|
||||
<li>Importing from Authy was flaky for entries that have an icon</li>
|
||||
<li>The dark theme was not properly applied to the QR code scanner view</li>
|
||||
<li>The app would crash on plain text export on some devices</li>
|
||||
<li>Importing from Authenticator Plus stopped working</li>
|
||||
</ul>
|
||||
<h3>Version 1.4.1</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Scanning QR codes stopped working on certain devices (primarily OnePlus)</li>
|
||||
</ul>
|
||||
<h3>Version 1.4</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Optionally delete the vault if a panic trigger is received from Ripple</li>
|
||||
<li>More customizable auto-lock</li>
|
||||
<li>More flexible export options
|
||||
<ul>
|
||||
<li>Share mechanism</li>
|
||||
<li>Offer to encrypt even if this feature is disabled in the app</li>
|
||||
<li>Export to a Google Authenticator URI file</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Perform exports/backups on a background thread (automatic backups now work with Nextcloud)</li>
|
||||
<li>Color improvements to the dark theme (slightly darker)</li>
|
||||
<li>Offer more locations to select an image/icon from</li>
|
||||
<li>Display some helpful information when importing from a different app</li>
|
||||
<li>Minimum tap to reveal timeout changed to 1 second</li>
|
||||
<li>After an entry is added, scroll to it and highlight it</li>
|
||||
<li>Updated translations, and new translations for: Basque, Chinese Traditional, Hindi, Indonesian, Japanese, Persian, Romanian, Slovak</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Scanning large images for QR codes would fail</li>
|
||||
<li>The FAB would remain hidden under certain circumstances</li>
|
||||
<li>The app would crash if an entry was added to the vault twice due to an IO error</li>
|
||||
<li>The app would crash if the device was rotated while a progress dialog was shown</li>
|
||||
<li>The PIN keyboard would show even if a new non-digit password was set</li>
|
||||
<li>The password reminder popup would be occluded by the autofill popup</li>
|
||||
<li>Importing from other apps on Android 11 was broken due to some permission issues</li>
|
||||
</ul>
|
||||
<h3>Version 1.3</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Completely rewritten intro/onboarding</li>
|
||||
<li>Option to show a PIN keyboard when unlocking Aegis</li>
|
||||
<li>A password strength meter when setting up encryption (based on zxcvbn)</li>
|
||||
<li>RTL support</li>
|
||||
<li>Arabic and Portuguese translations</li>
|
||||
<li>Updates to existing translations</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Better lifecycle handling of the biometric authentication prompt</li>
|
||||
<li>The filename of exported vaults had a double .json extension</li>
|
||||
<li>The navigation bar color was incorrect on devices pre API 27</li>
|
||||
<li>QR code scanner performance and stability improvements</li>
|
||||
<li>Various other small usability and stability improvements</li>
|
||||
</ul>
|
||||
<h3>Version 1.2.1</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Fix a rare issue where the intro could end up in a bad state</li>
|
||||
</ul>
|
||||
<h3>Version 1.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Add navigation bar color to themes</li>
|
||||
<li>Add support for importing from TOTP Authenticator</li>
|
||||
<li>Add support for importing from Microsoft Authenticator</li>
|
||||
<li>Add support for importing from Authenticator Plus</li>
|
||||
<li>Add support for importing a plain text Google Authenticator URI file</li>
|
||||
<li>Add support for importing from the new Google Authenticator export QR codes</li>
|
||||
<li>Add support for otpauth://steam URI's</li>
|
||||
<li>Add an option to copy tokens on tap (and disable it by default)</li>
|
||||
<li>Improve method to notify users on copy</li>
|
||||
<li>Add support for backups</li>
|
||||
<li>Improve multiselect flow</li>
|
||||
<li>Automatically adapt to system theme</li>
|
||||
<li>Add setting to change from 3 digit group size to 2 digit group size</li>
|
||||
<li>Use most frequent period to show progress</li>
|
||||
<li>Append a timestamp to the filename of exported vaults</li>
|
||||
<li>Add Hungarian translation</li>
|
||||
<li>Add Turkish translation</li>
|
||||
<li>Display a warning if automatic time sync is not enabled</li>
|
||||
<li>Minor card entry layout overhaul</li>
|
||||
<li>Ability to transfer tokens with qr codes</li>
|
||||
<li>Lockscreen overhaul</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Improve overall exception handling and error feedback to the user</li>
|
||||
<li>Improve icon editing flow</li>
|
||||
<li>Protect writes of the vault file against corruption with AtomicFile</li>
|
||||
<li>Make the parsing logic of the QR code URI more robust</li>
|
||||
<li>Importing from Authy now asks for password if needed</li>
|
||||
<li>Update Russian localization</li>
|
||||
<li>Increase password reminder period to 30 days</li>
|
||||
<li>Fix importing andOTP backups with more than 10000 PBKDF iterations</li>
|
||||
<li>Respect the global animator duration scale setting</li>
|
||||
</ul>
|
||||
<p>Various other minor improvements</p>
|
||||
<h3>Version 1.1.4</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>The export filename was missing the ".json" extension in some cases</li>
|
||||
</ul>
|
||||
<h3>Version 1.1.3</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Password reminder for users who use biometric unlock</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Tokens would not refresh in some rare cases</li>
|
||||
</ul>
|
||||
<h3>Version 1.1.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Ability to select multiple entries</li>
|
||||
<li>Ability to select a file location when exporting the vault (including cloud providers like Google Drive)</li>
|
||||
<li>Explanation and warning for the security options</li>
|
||||
<li>Removed external storage permissions</li>
|
||||
</ul>
|
||||
<h3>Version 1.1.1</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Exporting the vault did not work on Android 10</li>
|
||||
</ul>
|
||||
<h3>Version 1.1</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Support for other types of biometric authentication (i.e. Pixel 4 face unlock)</li>
|
||||
<li>Support for importing from WinAuth</li>
|
||||
<li>Support for Chromebooks</li>
|
||||
<li>Option to highlight entries when tapped</li>
|
||||
<li>Filter for ungrouped tokens</li>
|
||||
<li>Ability to search for token account names</li>
|
||||
<li>Simplified Chinese translation (thanks RunningMelos!)</li>
|
||||
<li>Updated translations (thanks to all Crowdin contributers!)</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>The behavior of highlighting and revealing entries was inconsistent</li>
|
||||
<li>The changelog dialog didn't work</li>
|
||||
<li>The persistent notification was shown even after the app was killed</li>
|
||||
</ul>
|
||||
<h3>Version 1.0.3</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Support for andOTP's new backup file format</li>
|
||||
</ul>
|
||||
<h3>Version 1.0.2</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Search feature on Huawei devices</li>
|
||||
</ul>
|
||||
<h4>Notes</h4>
|
||||
<ul>
|
||||
<li>Disabled automatic backups through the Google Play Store</li>
|
||||
</ul>
|
||||
<h3>Version 1.0.1</h3>
|
||||
<h4>Notes</h4>
|
||||
<ul>
|
||||
<li>Temporarily disabled search feature on Huawei devices</li>
|
||||
</ul>
|
||||
<h3>Version 1.0</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>New icon</li>
|
||||
<li>Overhaul of interaction with the entry list</li>
|
||||
<li>Persistent notification while the vault is unlocked</li>
|
||||
<li>Language override option</li>
|
||||
<li>Support for importing from FreeOTP+</li>
|
||||
<li>Ability to toggle password visibility during unlock</li>
|
||||
<li>Support for deeplinking otpauth URIs</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Bad overall performance and high battery usage</li>
|
||||
<li>Codes with an uneven number of digits are displayed incorrectly</li>
|
||||
<li>Crash when entering a large value for OTP period</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
14
app/src/main/assets/license.html
Normal file
14
app/src/main/assets/license.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<html>
|
||||
<head>
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: %2$s;
|
||||
color: %3$s;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<pre>%1$s</pre>
|
||||
</body>
|
||||
</html>
|
||||
BIN
app/src/main/ic_launcher-web.png
Normal file
BIN
app/src/main/ic_launcher-web.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
app/src/main/ic_launcher_debug-web.png
Normal file
BIN
app/src/main/ic_launcher_debug-web.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
22
app/src/main/java/com/amulyakhare/textdrawable/LICENSE
Normal file
22
app/src/main/java/com/amulyakhare/textdrawable/LICENSE
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Amulya Khare
|
||||
|
||||
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.
|
||||
|
||||
316
app/src/main/java/com/amulyakhare/textdrawable/TextDrawable.java
Normal file
316
app/src/main/java/com/amulyakhare/textdrawable/TextDrawable.java
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
package com.amulyakhare.textdrawable;
|
||||
|
||||
import android.graphics.*;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.OvalShape;
|
||||
import android.graphics.drawable.shapes.RectShape;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
|
||||
/**
|
||||
* @author amulya
|
||||
* @datetime 14 Oct 2014, 3:53 PM
|
||||
*/
|
||||
public class TextDrawable extends ShapeDrawable {
|
||||
|
||||
private final Paint textPaint;
|
||||
private final Paint borderPaint;
|
||||
private static final float SHADE_FACTOR = 0.9f;
|
||||
private final String text;
|
||||
private final int color;
|
||||
private final RectShape shape;
|
||||
private final int height;
|
||||
private final int width;
|
||||
private final int fontSize;
|
||||
private final float radius;
|
||||
private final int borderThickness;
|
||||
|
||||
private TextDrawable(Builder builder) {
|
||||
super(builder.shape);
|
||||
|
||||
// shape properties
|
||||
shape = builder.shape;
|
||||
height = builder.height;
|
||||
width = builder.width;
|
||||
radius = builder.radius;
|
||||
|
||||
// text and color
|
||||
text = builder.toUpperCase ? builder.text.toUpperCase() : builder.text;
|
||||
color = builder.color;
|
||||
|
||||
// text paint settings
|
||||
fontSize = builder.fontSize;
|
||||
textPaint = new Paint();
|
||||
textPaint.setColor(builder.textColor);
|
||||
textPaint.setAntiAlias(true);
|
||||
textPaint.setFakeBoldText(builder.isBold);
|
||||
textPaint.setStyle(Paint.Style.FILL);
|
||||
textPaint.setTypeface(builder.font);
|
||||
textPaint.setTextAlign(Paint.Align.CENTER);
|
||||
textPaint.setStrokeWidth(builder.borderThickness);
|
||||
|
||||
// border paint settings
|
||||
borderThickness = builder.borderThickness;
|
||||
borderPaint = new Paint();
|
||||
borderPaint.setColor(getDarkerShade(color));
|
||||
borderPaint.setStyle(Paint.Style.STROKE);
|
||||
borderPaint.setStrokeWidth(borderThickness);
|
||||
|
||||
// drawable paint color
|
||||
Paint paint = getPaint();
|
||||
paint.setColor(color);
|
||||
|
||||
}
|
||||
|
||||
private int getDarkerShade(int color) {
|
||||
return Color.rgb((int)(SHADE_FACTOR * Color.red(color)),
|
||||
(int)(SHADE_FACTOR * Color.green(color)),
|
||||
(int)(SHADE_FACTOR * Color.blue(color)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
super.draw(canvas);
|
||||
Rect r = getBounds();
|
||||
|
||||
|
||||
// draw border
|
||||
if (borderThickness > 0) {
|
||||
drawBorder(canvas);
|
||||
}
|
||||
|
||||
int count = canvas.save();
|
||||
canvas.translate(r.left, r.top);
|
||||
|
||||
// draw text
|
||||
int width = this.width < 0 ? r.width() : this.width;
|
||||
int height = this.height < 0 ? r.height() : this.height;
|
||||
int fontSize = this.fontSize < 0 ? (Math.min(width, height) / 2) : this.fontSize;
|
||||
textPaint.setTextSize(fontSize);
|
||||
canvas.drawText(text, width / 2, height / 2 - ((textPaint.descent() + textPaint.ascent()) / 2), textPaint);
|
||||
|
||||
canvas.restoreToCount(count);
|
||||
|
||||
}
|
||||
|
||||
private void drawBorder(Canvas canvas) {
|
||||
RectF rect = new RectF(getBounds());
|
||||
rect.inset(borderThickness/2, borderThickness/2);
|
||||
|
||||
if (shape instanceof OvalShape) {
|
||||
canvas.drawOval(rect, borderPaint);
|
||||
}
|
||||
else if (shape instanceof RoundRectShape) {
|
||||
canvas.drawRoundRect(rect, radius, radius, borderPaint);
|
||||
}
|
||||
else {
|
||||
canvas.drawRect(rect, borderPaint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
textPaint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter cf) {
|
||||
textPaint.setColorFilter(cf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public static IShapeBuilder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static class Builder implements IConfigBuilder, IShapeBuilder, IBuilder {
|
||||
|
||||
private String text;
|
||||
|
||||
private int color;
|
||||
|
||||
private int borderThickness;
|
||||
|
||||
private int width;
|
||||
|
||||
private int height;
|
||||
|
||||
private Typeface font;
|
||||
|
||||
private RectShape shape;
|
||||
|
||||
public int textColor;
|
||||
|
||||
private int fontSize;
|
||||
|
||||
private boolean isBold;
|
||||
|
||||
private boolean toUpperCase;
|
||||
|
||||
public float radius;
|
||||
|
||||
private Builder() {
|
||||
text = "";
|
||||
color = Color.GRAY;
|
||||
textColor = Color.WHITE;
|
||||
borderThickness = 0;
|
||||
width = -1;
|
||||
height = -1;
|
||||
shape = new RectShape();
|
||||
font = Typeface.create("sans-serif-light", Typeface.NORMAL);
|
||||
fontSize = -1;
|
||||
isBold = false;
|
||||
toUpperCase = false;
|
||||
}
|
||||
|
||||
public IConfigBuilder width(int width) {
|
||||
this.width = width;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IConfigBuilder height(int height) {
|
||||
this.height = height;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IConfigBuilder textColor(int color) {
|
||||
this.textColor = color;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IConfigBuilder withBorder(int thickness) {
|
||||
this.borderThickness = thickness;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IConfigBuilder useFont(Typeface font) {
|
||||
this.font = font;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IConfigBuilder fontSize(int size) {
|
||||
this.fontSize = size;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IConfigBuilder bold() {
|
||||
this.isBold = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IConfigBuilder toUpperCase() {
|
||||
this.toUpperCase = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IConfigBuilder beginConfig() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IShapeBuilder endConfig() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBuilder rect() {
|
||||
this.shape = new RectShape();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBuilder round() {
|
||||
this.shape = new OvalShape();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBuilder roundRect(int radius) {
|
||||
this.radius = radius;
|
||||
float[] radii = {radius, radius, radius, radius, radius, radius, radius, radius};
|
||||
this.shape = new RoundRectShape(radii, null, null);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextDrawable buildRect(String text, int color) {
|
||||
rect();
|
||||
return build(text, color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextDrawable buildRoundRect(String text, int color, int radius) {
|
||||
roundRect(radius);
|
||||
return build(text, color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextDrawable buildRound(String text, int color) {
|
||||
round();
|
||||
return build(text, color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextDrawable build(String text, int color) {
|
||||
this.color = color;
|
||||
this.text = text;
|
||||
return new TextDrawable(this);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IConfigBuilder {
|
||||
public IConfigBuilder width(int width);
|
||||
|
||||
public IConfigBuilder height(int height);
|
||||
|
||||
public IConfigBuilder textColor(int color);
|
||||
|
||||
public IConfigBuilder withBorder(int thickness);
|
||||
|
||||
public IConfigBuilder useFont(Typeface font);
|
||||
|
||||
public IConfigBuilder fontSize(int size);
|
||||
|
||||
public IConfigBuilder bold();
|
||||
|
||||
public IConfigBuilder toUpperCase();
|
||||
|
||||
public IShapeBuilder endConfig();
|
||||
}
|
||||
|
||||
public static interface IBuilder {
|
||||
|
||||
public TextDrawable build(String text, int color);
|
||||
}
|
||||
|
||||
public static interface IShapeBuilder {
|
||||
|
||||
public IConfigBuilder beginConfig();
|
||||
|
||||
public IBuilder rect();
|
||||
|
||||
public IBuilder round();
|
||||
|
||||
public IBuilder roundRect(int radius);
|
||||
|
||||
public TextDrawable buildRect(String text, int color);
|
||||
|
||||
public TextDrawable buildRoundRect(String text, int color, int radius);
|
||||
|
||||
public TextDrawable buildRound(String text, int color);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package com.amulyakhare.textdrawable.util;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* @author amulya
|
||||
* @datetime 14 Oct 2014, 5:20 PM
|
||||
*/
|
||||
public class ColorGenerator {
|
||||
|
||||
public static ColorGenerator DEFAULT;
|
||||
|
||||
public static ColorGenerator MATERIAL;
|
||||
|
||||
static {
|
||||
DEFAULT = create(Arrays.asList(
|
||||
0xfff16364,
|
||||
0xfff58559,
|
||||
0xfff9a43e,
|
||||
0xffe4c62e,
|
||||
0xff67bf74,
|
||||
0xff59a2be,
|
||||
0xff2093cd,
|
||||
0xffad62a7,
|
||||
0xff805781
|
||||
));
|
||||
MATERIAL = create(Arrays.asList(
|
||||
0xffe57373,
|
||||
0xfff06292,
|
||||
0xffba68c8,
|
||||
0xff9575cd,
|
||||
0xff7986cb,
|
||||
0xff64b5f6,
|
||||
0xff4fc3f7,
|
||||
0xff4dd0e1,
|
||||
0xff4db6ac,
|
||||
0xff81c784,
|
||||
0xffaed581,
|
||||
0xffff8a65,
|
||||
0xffd4e157,
|
||||
0xffffd54f,
|
||||
0xffffb74d,
|
||||
0xffa1887f,
|
||||
0xff90a4ae
|
||||
));
|
||||
}
|
||||
|
||||
private final List<Integer> mColors;
|
||||
private final Random mRandom;
|
||||
|
||||
public static ColorGenerator create(List<Integer> colorList) {
|
||||
return new ColorGenerator(colorList);
|
||||
}
|
||||
|
||||
private ColorGenerator(List<Integer> colorList) {
|
||||
mColors = colorList;
|
||||
mRandom = new Random(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public int getRandomColor() {
|
||||
return mColors.get(mRandom.nextInt(mColors.size()));
|
||||
}
|
||||
|
||||
public int getColor(Object key) {
|
||||
return mColors.get(Math.abs(key.hashCode()) % mColors.size());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
public enum AccountNamePosition {
|
||||
HIDDEN,
|
||||
END,
|
||||
BELOW;
|
||||
|
||||
private static AccountNamePosition[] _values;
|
||||
|
||||
static {
|
||||
_values = values();
|
||||
}
|
||||
|
||||
public static AccountNamePosition fromInteger(int x) {
|
||||
return _values[x];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import dagger.hilt.android.HiltAndroidApp;
|
||||
|
||||
@HiltAndroidApp
|
||||
public class AegisApplication extends AegisApplicationBase {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.ShortcutInfo;
|
||||
import android.content.pm.ShortcutManager;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleEventObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
|
||||
import com.beemdevelopment.aegis.receivers.VaultLockReceiver;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import dagger.hilt.InstallIn;
|
||||
import dagger.hilt.android.EarlyEntryPoint;
|
||||
import dagger.hilt.android.EarlyEntryPoints;
|
||||
import dagger.hilt.components.SingletonComponent;
|
||||
|
||||
public abstract class AegisApplicationBase extends Application {
|
||||
private static final String CODE_LOCK_STATUS_ID = "lock_status_channel";
|
||||
|
||||
private VaultManager _vaultManager;
|
||||
|
||||
static {
|
||||
// Enable verbose libsu logging in debug builds
|
||||
Shell.enableVerboseLogging = BuildConfig.DEBUG;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
_vaultManager = EarlyEntryPoints.get(this, EntryPoint.class).getVaultManager();
|
||||
|
||||
VaultLockReceiver lockReceiver = new VaultLockReceiver();
|
||||
IntentFilter intentFilter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
|
||||
ContextCompat.registerReceiver(this, lockReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED);
|
||||
|
||||
// lock the app if the user moves the application to the background
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(new AppLifecycleObserver());
|
||||
|
||||
// clear the cache directory on startup, to make sure no temporary vault export files remain
|
||||
IOUtils.clearDirectory(getCacheDir(), false);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
initAppShortcuts();
|
||||
}
|
||||
|
||||
// NOTE: Disabled for now. See issue: #1047
|
||||
/*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
initNotificationChannels();
|
||||
}*/
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N_MR1)
|
||||
private void initAppShortcuts() {
|
||||
ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);
|
||||
if (shortcutManager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.putExtra("action", "scan");
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
intent.setAction(Intent.ACTION_MAIN);
|
||||
|
||||
ShortcutInfo shortcut = new ShortcutInfo.Builder(this, "shortcut_new")
|
||||
.setShortLabel(getString(R.string.new_entry))
|
||||
.setLongLabel(getString(R.string.add_new_entry))
|
||||
.setIcon(Icon.createWithResource(this, R.drawable.ic_qr_code))
|
||||
.setIntent(intent)
|
||||
.build();
|
||||
|
||||
shortcutManager.setDynamicShortcuts(Collections.singletonList(shortcut));
|
||||
}
|
||||
|
||||
private void initNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
CharSequence name = getString(R.string.channel_name_lock_status);
|
||||
String description = getString(R.string.channel_description_lock_status);
|
||||
int importance = NotificationManager.IMPORTANCE_LOW;
|
||||
|
||||
NotificationChannel channel = new NotificationChannel(CODE_LOCK_STATUS_ID, name, importance);
|
||||
channel.setDescription(description);
|
||||
|
||||
NotificationManager notificationManager = getSystemService(NotificationManager.class);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
private class AppLifecycleObserver implements LifecycleEventObserver {
|
||||
@Override
|
||||
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
|
||||
if (event == Lifecycle.Event.ON_STOP
|
||||
&& _vaultManager.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_MINIMIZE)
|
||||
&& !_vaultManager.isAutoLockBlocked()) {
|
||||
_vaultManager.lock(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EarlyEntryPoint
|
||||
@InstallIn(SingletonComponent.class)
|
||||
interface EntryPoint {
|
||||
VaultManager getVaultManager();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.app.backup.BackupAgent;
|
||||
import android.app.backup.BackupDataInput;
|
||||
import android.app.backup.BackupDataOutput;
|
||||
import android.app.backup.FullBackupDataOutput;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
|
||||
import com.beemdevelopment.aegis.database.AppDatabase;
|
||||
import com.beemdevelopment.aegis.database.AuditLogRepository;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class AegisBackupAgent extends BackupAgent {
|
||||
private static final String TAG = AegisBackupAgent.class.getSimpleName();
|
||||
|
||||
private Preferences _prefs;
|
||||
|
||||
private AuditLogRepository _auditLogRepository;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
// Cannot use injection with Dagger Hilt here, because the app is launched in a restricted mode on restore
|
||||
_prefs = new Preferences(this);
|
||||
AppDatabase appDatabase = AegisModule.provideAppDatabase(this);
|
||||
_auditLogRepository = AegisModule.provideAuditLogRepository(appDatabase);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onFullBackup(FullBackupDataOutput data) throws IOException {
|
||||
Log.i(TAG, String.format("onFullBackup() called: flags=%d, quota=%d",
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? data.getTransportFlags() : -1,
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? data.getQuota() : -1));
|
||||
|
||||
boolean isD2D = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||
&& (data.getTransportFlags() & FLAG_DEVICE_TO_DEVICE_TRANSFER) == FLAG_DEVICE_TO_DEVICE_TRANSFER;
|
||||
|
||||
if (isD2D) {
|
||||
Log.i(TAG, "onFullBackup(): allowing D2D transfer");
|
||||
} else if (!_prefs.isAndroidBackupsEnabled()) {
|
||||
Log.i(TAG, "onFullBackup() skipped: Android backups disabled in preferences");
|
||||
return;
|
||||
}
|
||||
|
||||
// We perform a catch of any Exception here to make sure we also
|
||||
// report any runtime exceptions, in addition to the expected IOExceptions.
|
||||
try {
|
||||
fullBackup(data);
|
||||
_auditLogRepository.addAndroidBackupCreatedEvent();
|
||||
_prefs.setAndroidBackupResult(new Preferences.BackupResult(null));
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
|
||||
_prefs.setAndroidBackupResult(new Preferences.BackupResult(e));
|
||||
throw e;
|
||||
}
|
||||
|
||||
Log.i(TAG, "onFullBackup() finished");
|
||||
}
|
||||
|
||||
private void fullBackup(FullBackupDataOutput data) throws IOException {
|
||||
// First copy the vault to the files/backup directory
|
||||
createBackupDir();
|
||||
File vaultBackupFile = getVaultBackupFile();
|
||||
try (OutputStream outputStream = new FileOutputStream(vaultBackupFile)) {
|
||||
VaultFile vaultFile = VaultRepository.readVaultFile(this);
|
||||
byte[] bytes = vaultFile.exportable().toBytes();
|
||||
outputStream.write(bytes);
|
||||
} catch (VaultRepositoryException | IOException e) {
|
||||
deleteBackupDir();
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
// Then call the original implementation so that fullBackupContent specified in AndroidManifest is read
|
||||
try {
|
||||
super.onFullBackup(data);
|
||||
} finally {
|
||||
deleteBackupDir();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onRestoreFile(ParcelFileDescriptor data, long size, File destination, int type, long mode, long mtime) throws IOException {
|
||||
Log.i(TAG, String.format("onRestoreFile() called: dest=%s", destination));
|
||||
super.onRestoreFile(data, size, destination, type, mode, mtime);
|
||||
|
||||
File vaultBackupFile = getVaultBackupFile();
|
||||
if (destination.getCanonicalFile().equals(vaultBackupFile.getCanonicalFile())) {
|
||||
try (InputStream inStream = new FileInputStream(vaultBackupFile)) {
|
||||
VaultRepository.writeToFile(this, inStream);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, String.format("onRestoreFile() failed: dest=%s, error=%s", destination, e));
|
||||
throw e;
|
||||
} finally {
|
||||
deleteBackupDir();
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, String.format("onRestoreFile() finished: dest=%s", destination));
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onQuotaExceeded(long backupDataBytes, long quotaBytes) {
|
||||
super.onQuotaExceeded(backupDataBytes, quotaBytes);
|
||||
Log.e(TAG, String.format("onQuotaExceeded() called: backupDataBytes=%d, quotaBytes=%d", backupDataBytes, quotaBytes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) throws IOException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException {
|
||||
|
||||
}
|
||||
|
||||
private void createBackupDir() throws IOException {
|
||||
File dir = getVaultBackupFile().getParentFile();
|
||||
if (dir == null || (!dir.exists() && !dir.mkdir())) {
|
||||
throw new IOException(String.format("Unable to create backup directory: %s", dir));
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteBackupDir() {
|
||||
File dir = getVaultBackupFile().getParentFile();
|
||||
if (dir != null) {
|
||||
IOUtils.clearDirectory(dir, true);
|
||||
}
|
||||
}
|
||||
|
||||
private File getVaultBackupFile() {
|
||||
return new File(new File(getFilesDir(), "backup"), VaultRepository.FILENAME);
|
||||
}
|
||||
}
|
||||
55
app/src/main/java/com/beemdevelopment/aegis/AegisModule.java
Normal file
55
app/src/main/java/com/beemdevelopment/aegis/AegisModule.java
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.room.Room;
|
||||
|
||||
import com.beemdevelopment.aegis.database.AppDatabase;
|
||||
import com.beemdevelopment.aegis.database.AuditLogDao;
|
||||
import com.beemdevelopment.aegis.database.AuditLogRepository;
|
||||
import com.beemdevelopment.aegis.icons.IconPackManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.hilt.InstallIn;
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext;
|
||||
import dagger.hilt.components.SingletonComponent;
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent.class)
|
||||
public class AegisModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
public static IconPackManager provideIconPackManager(@ApplicationContext Context context) {
|
||||
return new IconPackManager(context);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static AuditLogRepository provideAuditLogRepository(AppDatabase appDatabase) {
|
||||
AuditLogDao auditLogDao = appDatabase.auditLogDao();
|
||||
return new AuditLogRepository(auditLogDao);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static VaultManager provideVaultManager(@ApplicationContext Context context, AuditLogRepository auditLogRepository) {
|
||||
return new VaultManager(context, auditLogRepository);
|
||||
}
|
||||
|
||||
@Provides
|
||||
public static Preferences providePreferences(@ApplicationContext Context context) {
|
||||
return new Preferences(context);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static AppDatabase provideAppDatabase(@ApplicationContext Context context) {
|
||||
return Room.databaseBuilder(context.getApplicationContext(),
|
||||
AppDatabase.class, "aegis-db")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
public enum BackupsVersioningStrategy {
|
||||
UNDEFINED,
|
||||
MULTIPLE_BACKUPS,
|
||||
SINGLE_BACKUP
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
public enum CopyBehavior {
|
||||
NEVER,
|
||||
SINGLETAP,
|
||||
DOUBLETAP;
|
||||
|
||||
private static CopyBehavior[] _values;
|
||||
|
||||
static {
|
||||
_values = values();
|
||||
}
|
||||
|
||||
public static CopyBehavior fromInteger(int x) {
|
||||
return _values[x];
|
||||
}
|
||||
}
|
||||
42
app/src/main/java/com/beemdevelopment/aegis/EventType.java
Normal file
42
app/src/main/java/com/beemdevelopment/aegis/EventType.java
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
public enum EventType {
|
||||
|
||||
VAULT_UNLOCKED,
|
||||
VAULT_BACKUP_CREATED,
|
||||
VAULT_ANDROID_BACKUP_CREATED,
|
||||
VAULT_EXPORTED,
|
||||
ENTRY_SHARED,
|
||||
VAULT_UNLOCK_FAILED_PASSWORD,
|
||||
VAULT_UNLOCK_FAILED_BIOMETRICS;
|
||||
private static EventType[] _values;
|
||||
|
||||
static {
|
||||
_values = values();
|
||||
}
|
||||
|
||||
public static EventType fromInteger(int x) {
|
||||
return _values[x];
|
||||
}
|
||||
|
||||
public static int getEventTitleRes(EventType eventType) {
|
||||
switch (eventType) {
|
||||
case VAULT_UNLOCKED:
|
||||
return R.string.event_title_vault_unlocked;
|
||||
case VAULT_BACKUP_CREATED:
|
||||
return R.string.event_title_backup_created;
|
||||
case VAULT_ANDROID_BACKUP_CREATED:
|
||||
return R.string.event_title_android_backup_created;
|
||||
case VAULT_EXPORTED:
|
||||
return R.string.event_title_vault_exported;
|
||||
case ENTRY_SHARED:
|
||||
return R.string.event_title_entry_shared;
|
||||
case VAULT_UNLOCK_FAILED_PASSWORD:
|
||||
return R.string.event_title_vault_unlock_failed_password;
|
||||
case VAULT_UNLOCK_FAILED_BIOMETRICS:
|
||||
return R.string.event_title_vault_unlock_failed_biometrics;
|
||||
default:
|
||||
return R.string.event_unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
public enum GroupPlaceholderType {
|
||||
ALL,
|
||||
NEW_GROUP,
|
||||
NO_GROUP;
|
||||
|
||||
public int getStringRes() {
|
||||
switch (this) {
|
||||
case ALL:
|
||||
return R.string.all;
|
||||
case NEW_GROUP:
|
||||
return R.string.new_group;
|
||||
case NO_GROUP:
|
||||
return R.string.no_group;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unexpected placeholder type: " + this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public enum PassReminderFreq {
|
||||
NEVER,
|
||||
WEEKLY,
|
||||
BIWEEKLY,
|
||||
MONTHLY,
|
||||
QUARTERLY;
|
||||
|
||||
public long getDurationMillis() {
|
||||
long weeks;
|
||||
switch (this) {
|
||||
case WEEKLY:
|
||||
weeks = 1;
|
||||
break;
|
||||
case BIWEEKLY:
|
||||
weeks = 2;
|
||||
break;
|
||||
case MONTHLY:
|
||||
weeks = 4;
|
||||
break;
|
||||
case QUARTERLY:
|
||||
weeks = 13;
|
||||
break;
|
||||
default:
|
||||
weeks = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
return TimeUnit.MILLISECONDS.convert(weeks * 7L, TimeUnit.DAYS);
|
||||
}
|
||||
|
||||
@StringRes
|
||||
public int getStringRes() {
|
||||
switch (this) {
|
||||
case WEEKLY:
|
||||
return R.string.password_reminder_freq_weekly;
|
||||
case BIWEEKLY:
|
||||
return R.string.password_reminder_freq_biweekly;
|
||||
case MONTHLY:
|
||||
return R.string.password_reminder_freq_monthly;
|
||||
case QUARTERLY:
|
||||
return R.string.password_reminder_freq_quarterly;
|
||||
default:
|
||||
return R.string.password_reminder_freq_never;
|
||||
}
|
||||
}
|
||||
|
||||
public static PassReminderFreq fromInteger(int i) {
|
||||
return PassReminderFreq.values()[i];
|
||||
}
|
||||
}
|
||||
707
app/src/main/java/com/beemdevelopment/aegis/Preferences.java
Normal file
707
app/src/main/java/com/beemdevelopment/aegis/Preferences.java
Normal file
|
|
@ -0,0 +1,707 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.provider.DocumentsContractCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.beemdevelopment.aegis.util.JsonUtils;
|
||||
import com.beemdevelopment.aegis.util.TimeUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultBackupPermissionException;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class Preferences {
|
||||
public static final int AUTO_LOCK_OFF = 1 << 0;
|
||||
public static final int AUTO_LOCK_ON_BACK_BUTTON = 1 << 1;
|
||||
public static final int AUTO_LOCK_ON_MINIMIZE = 1 << 2;
|
||||
public static final int AUTO_LOCK_ON_DEVICE_LOCK = 1 << 3;
|
||||
|
||||
public static final int SEARCH_IN_ISSUER = 1 << 0;
|
||||
public static final int SEARCH_IN_NAME = 1 << 1;
|
||||
public static final int SEARCH_IN_NOTE = 1 << 2;
|
||||
public static final int SEARCH_IN_GROUPS = 1 << 3;
|
||||
|
||||
public static final int BACKUPS_VERSIONS_INFINITE = -1;
|
||||
|
||||
public static final int[] AUTO_LOCK_SETTINGS = {
|
||||
AUTO_LOCK_ON_BACK_BUTTON,
|
||||
AUTO_LOCK_ON_MINIMIZE,
|
||||
AUTO_LOCK_ON_DEVICE_LOCK
|
||||
};
|
||||
|
||||
public static final int[] SEARCH_BEHAVIOR_SETTINGS = {
|
||||
SEARCH_IN_ISSUER,
|
||||
SEARCH_IN_NAME,
|
||||
SEARCH_IN_NOTE,
|
||||
SEARCH_IN_GROUPS
|
||||
};
|
||||
|
||||
private SharedPreferences _prefs;
|
||||
|
||||
public Preferences(Context context) {
|
||||
_prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
if (getPasswordReminderTimestamp().getTime() == 0) {
|
||||
resetPasswordReminderTimestamp();
|
||||
}
|
||||
|
||||
migratePreferences();
|
||||
}
|
||||
|
||||
public void migratePreferences() {
|
||||
// Change copy on tap to copy behavior to new preference and delete the old key
|
||||
String prefCopyOnTapKey = "pref_copy_on_tap";
|
||||
if (_prefs.contains(prefCopyOnTapKey)) {
|
||||
|
||||
boolean isCopyOnTapEnabled = _prefs.getBoolean(prefCopyOnTapKey, false);
|
||||
if (isCopyOnTapEnabled) {
|
||||
setCopyBehavior(CopyBehavior.SINGLETAP);
|
||||
}
|
||||
|
||||
_prefs.edit().remove(prefCopyOnTapKey).apply();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isTapToRevealEnabled() {
|
||||
return _prefs.getBoolean("pref_tap_to_reveal", false);
|
||||
}
|
||||
|
||||
public boolean isGroupMultiselectEnabled() {
|
||||
return _prefs.getBoolean("pref_groups_multiselect", false);
|
||||
}
|
||||
|
||||
public boolean isEntryHighlightEnabled() {
|
||||
return _prefs.getBoolean("pref_highlight_entry", false);
|
||||
}
|
||||
|
||||
public boolean isHapticFeedbackEnabled() {
|
||||
return _prefs.getBoolean("pref_haptic_feedback", true);
|
||||
}
|
||||
|
||||
public boolean isPauseFocusedEnabled() {
|
||||
boolean dependenciesEnabled = isTapToRevealEnabled() || isEntryHighlightEnabled();
|
||||
if (!dependenciesEnabled) return false;
|
||||
return _prefs.getBoolean("pref_pause_entry", false);
|
||||
}
|
||||
|
||||
public boolean isPanicTriggerEnabled() {
|
||||
return _prefs.getBoolean("pref_panic_trigger", false);
|
||||
}
|
||||
|
||||
public void setIsPanicTriggerEnabled(boolean enabled) {
|
||||
_prefs.edit().putBoolean("pref_panic_trigger", enabled).apply();
|
||||
}
|
||||
|
||||
public boolean isSecureScreenEnabled() {
|
||||
// screen security should be enabled by default, but not for debug builds
|
||||
return _prefs.getBoolean("pref_secure_screen", !BuildConfig.DEBUG);
|
||||
}
|
||||
|
||||
public PassReminderFreq getPasswordReminderFrequency() {
|
||||
final String key = "pref_password_reminder_freq";
|
||||
if (_prefs.contains(key) || _prefs.getBoolean("pref_password_reminder", true)) {
|
||||
int i = _prefs.getInt(key, PassReminderFreq.BIWEEKLY.ordinal());
|
||||
return PassReminderFreq.fromInteger(i);
|
||||
}
|
||||
|
||||
return PassReminderFreq.NEVER;
|
||||
}
|
||||
|
||||
public void setPasswordReminderFrequency(PassReminderFreq freq) {
|
||||
_prefs.edit().putInt("pref_password_reminder_freq", freq.ordinal()).apply();
|
||||
}
|
||||
|
||||
public boolean isPasswordReminderNeeded() {
|
||||
return isPasswordReminderNeeded(new Date().getTime());
|
||||
}
|
||||
|
||||
boolean isPasswordReminderNeeded(long currTime) {
|
||||
PassReminderFreq freq = getPasswordReminderFrequency();
|
||||
if (freq == PassReminderFreq.NEVER) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long duration = currTime - getPasswordReminderTimestamp().getTime();
|
||||
return duration >= freq.getDurationMillis();
|
||||
}
|
||||
|
||||
public Date getPasswordReminderTimestamp() {
|
||||
return new Date(_prefs.getLong("pref_password_reminder_counter", 0));
|
||||
}
|
||||
|
||||
void setPasswordReminderTimestamp(long timestamp) {
|
||||
_prefs.edit().putLong("pref_password_reminder_counter", timestamp).apply();
|
||||
}
|
||||
|
||||
public void resetPasswordReminderTimestamp() {
|
||||
setPasswordReminderTimestamp(new Date().getTime());
|
||||
}
|
||||
|
||||
public boolean onlyShowNecessaryAccountNames() { return _prefs.getBoolean("pref_shared_issuer_account_name", false); }
|
||||
|
||||
public boolean isIconVisible() {
|
||||
return _prefs.getBoolean("pref_show_icons", true);
|
||||
}
|
||||
|
||||
public boolean getShowNextCode() {
|
||||
return _prefs.getBoolean("pref_show_next_code", false);
|
||||
}
|
||||
|
||||
public boolean getShowExpirationState() {
|
||||
return _prefs.getBoolean("pref_expiration_state", true);
|
||||
}
|
||||
|
||||
public CodeGrouping getCodeGroupSize() {
|
||||
String value = _prefs.getString("pref_code_group_size_string", "GROUPING_THREES");
|
||||
|
||||
return CodeGrouping.valueOf(value);
|
||||
}
|
||||
|
||||
public void setCodeGroupSize(CodeGrouping codeGroupSize) {
|
||||
_prefs.edit().putString("pref_code_group_size_string", codeGroupSize.name()).apply();
|
||||
}
|
||||
|
||||
public boolean isIntroDone() {
|
||||
return _prefs.getBoolean("pref_intro", false);
|
||||
}
|
||||
|
||||
private int getAutoLockMask() {
|
||||
final int def = AUTO_LOCK_ON_BACK_BUTTON | AUTO_LOCK_ON_DEVICE_LOCK;
|
||||
if (!_prefs.contains("pref_auto_lock_mask")) {
|
||||
return _prefs.getBoolean("pref_auto_lock", true) ? def : AUTO_LOCK_OFF;
|
||||
}
|
||||
|
||||
return _prefs.getInt("pref_auto_lock_mask", def);
|
||||
}
|
||||
|
||||
public int getSearchBehaviorMask() {
|
||||
final int def = SEARCH_IN_ISSUER | SEARCH_IN_NAME;
|
||||
|
||||
return _prefs.getInt("pref_search_behavior_mask", def);
|
||||
}
|
||||
|
||||
public boolean isSearchBehaviorTypeEnabled(int searchBehaviorType) {
|
||||
return (getSearchBehaviorMask() & searchBehaviorType) == searchBehaviorType;
|
||||
}
|
||||
|
||||
public void setSearchBehaviorMask(int searchBehavior) {
|
||||
_prefs.edit().putInt("pref_search_behavior_mask", searchBehavior).apply();
|
||||
}
|
||||
|
||||
public boolean isAutoLockEnabled() {
|
||||
return getAutoLockMask() != AUTO_LOCK_OFF;
|
||||
}
|
||||
|
||||
public boolean isAutoLockTypeEnabled(int autoLockType) {
|
||||
return (getAutoLockMask() & autoLockType) == autoLockType;
|
||||
}
|
||||
|
||||
public void setAutoLockMask(int autoLock) {
|
||||
_prefs.edit().putInt("pref_auto_lock_mask", autoLock).apply();
|
||||
}
|
||||
|
||||
public void setIntroDone(boolean done) {
|
||||
_prefs.edit().putBoolean("pref_intro", done).apply();
|
||||
}
|
||||
|
||||
public void setTapToRevealTime(int number) {
|
||||
_prefs.edit().putInt("pref_tap_to_reveal_time", number).apply();
|
||||
}
|
||||
|
||||
public void setCurrentSortCategory(SortCategory category) {
|
||||
_prefs.edit().putInt("pref_current_sort_category", category.ordinal()).apply();
|
||||
}
|
||||
|
||||
public SortCategory getCurrentSortCategory() {
|
||||
return SortCategory.fromInteger(_prefs.getInt("pref_current_sort_category", 0));
|
||||
}
|
||||
|
||||
public int getTapToRevealTime() {
|
||||
return _prefs.getInt("pref_tap_to_reveal_time", 30);
|
||||
}
|
||||
|
||||
public Theme getCurrentTheme() {
|
||||
return Theme.fromInteger(_prefs.getInt("pref_current_theme", Theme.SYSTEM.ordinal()));
|
||||
}
|
||||
|
||||
public void setCurrentTheme(Theme theme) {
|
||||
_prefs.edit().putInt("pref_current_theme", theme.ordinal()).apply();
|
||||
}
|
||||
|
||||
public boolean isDynamicColorsEnabled() {
|
||||
return _prefs.getBoolean("pref_dynamic_colors", false);
|
||||
}
|
||||
|
||||
public ViewMode getCurrentViewMode() {
|
||||
return ViewMode.fromInteger(_prefs.getInt("pref_current_view_mode", 0));
|
||||
}
|
||||
|
||||
public void setCurrentViewMode(ViewMode viewMode) {
|
||||
_prefs.edit().putInt("pref_current_view_mode", viewMode.ordinal()).apply();
|
||||
}
|
||||
|
||||
public AccountNamePosition getAccountNamePosition() {
|
||||
return AccountNamePosition.fromInteger(_prefs.getInt("pref_account_name_position", AccountNamePosition.END.ordinal()));
|
||||
}
|
||||
|
||||
public void setAccountNamePosition(AccountNamePosition accountNamePosition) {
|
||||
_prefs.edit().putInt("pref_account_name_position", accountNamePosition.ordinal()).apply();
|
||||
}
|
||||
|
||||
public Integer getUsageCount(UUID uuid) {
|
||||
Integer usageCount = getUsageCounts().get(uuid);
|
||||
|
||||
return usageCount != null ? usageCount : 0;
|
||||
}
|
||||
|
||||
public void resetUsageCount(UUID uuid) {
|
||||
Map<UUID, Integer> usageCounts = getUsageCounts();
|
||||
usageCounts.put(uuid, 0);
|
||||
|
||||
setUsageCount(usageCounts);
|
||||
}
|
||||
|
||||
public long getLastUsedTimestamp(UUID uuid) {
|
||||
Map<UUID, Long> timestamps = getLastUsedTimestamps();
|
||||
if (timestamps != null && timestamps.size() > 0){
|
||||
Long timestamp = timestamps.get(uuid);
|
||||
return timestamp != null ? timestamp : 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void clearUsageCount() {
|
||||
_prefs.edit().remove("pref_usage_count").apply();
|
||||
}
|
||||
|
||||
public Map<UUID, Long> getLastUsedTimestamps() {
|
||||
Map<UUID, Long> lastUsedTimestamps = new HashMap<>();
|
||||
String lastUsedTimestamp = _prefs.getString("pref_last_used_timestamps", "");
|
||||
try {
|
||||
JSONArray arr = new JSONArray(lastUsedTimestamp);
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
JSONObject json = arr.getJSONObject(i);
|
||||
lastUsedTimestamps.put(UUID.fromString(json.getString("uuid")), json.getLong("timestamp"));
|
||||
}
|
||||
} catch (JSONException ignored) {
|
||||
}
|
||||
|
||||
return lastUsedTimestamps;
|
||||
}
|
||||
|
||||
public void setLastUsedTimestamps(Map<UUID, Long> lastUsedTimestamps) {
|
||||
JSONArray lastUsedTimestampJson = new JSONArray();
|
||||
for (Map.Entry<UUID, Long> entry : lastUsedTimestamps.entrySet()) {
|
||||
JSONObject entryJson = new JSONObject();
|
||||
try {
|
||||
entryJson.put("uuid", entry.getKey());
|
||||
entryJson.put("timestamp", entry.getValue());
|
||||
lastUsedTimestampJson.put(entryJson);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
_prefs.edit().putString("pref_last_used_timestamps", lastUsedTimestampJson.toString()).apply();
|
||||
}
|
||||
|
||||
public Map<UUID, Integer> getUsageCounts() {
|
||||
Map<UUID, Integer> usageCounts = new HashMap<>();
|
||||
String usageCount = _prefs.getString("pref_usage_count", "");
|
||||
try {
|
||||
JSONArray arr = new JSONArray(usageCount);
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
JSONObject json = arr.getJSONObject(i);
|
||||
usageCounts.put(UUID.fromString(json.getString("uuid")), json.getInt("count"));
|
||||
}
|
||||
} catch (JSONException ignored) {
|
||||
}
|
||||
|
||||
return usageCounts;
|
||||
}
|
||||
|
||||
public void setUsageCount(Map<UUID, Integer> usageCounts) {
|
||||
JSONArray usageCountJson = new JSONArray();
|
||||
for (Map.Entry<UUID, Integer> entry : usageCounts.entrySet()) {
|
||||
JSONObject entryJson = new JSONObject();
|
||||
try {
|
||||
entryJson.put("uuid", entry.getKey());
|
||||
entryJson.put("count", entry.getValue());
|
||||
usageCountJson.put(entryJson);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
_prefs.edit().putString("pref_usage_count", usageCountJson.toString()).apply();
|
||||
}
|
||||
|
||||
public int getTimeout() {
|
||||
return _prefs.getInt("pref_timeout", -1);
|
||||
}
|
||||
|
||||
public String getLanguage() {
|
||||
return _prefs.getString("pref_lang", "system");
|
||||
}
|
||||
|
||||
public void setLanguage(String lang) {
|
||||
_prefs.edit().putString("pref_lang", lang).apply();
|
||||
}
|
||||
|
||||
public Locale getLocale() {
|
||||
String lang = getLanguage();
|
||||
|
||||
if (lang.equals("system")) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
return Resources.getSystem().getConfiguration().getLocales().get(0);
|
||||
} else {
|
||||
return Resources.getSystem().getConfiguration().locale;
|
||||
}
|
||||
}
|
||||
|
||||
String[] parts = lang.split("_");
|
||||
if (parts.length == 1) {
|
||||
return new Locale(parts[0]);
|
||||
}
|
||||
|
||||
return new Locale(parts[0], parts[1]);
|
||||
}
|
||||
|
||||
public boolean isAndroidBackupsEnabled() {
|
||||
return _prefs.getBoolean("pref_android_backups", false);
|
||||
}
|
||||
|
||||
public void setIsAndroidBackupsEnabled(boolean enabled) {
|
||||
_prefs.edit().putBoolean("pref_android_backups", enabled).apply();
|
||||
setAndroidBackupResult(null);
|
||||
}
|
||||
|
||||
public boolean isBackupsEnabled() {
|
||||
return _prefs.getBoolean("pref_backups", false);
|
||||
}
|
||||
|
||||
public void setIsBackupsEnabled(boolean enabled) {
|
||||
_prefs.edit().putBoolean("pref_backups", enabled).apply();
|
||||
setBuiltInBackupResult(null);
|
||||
}
|
||||
|
||||
public boolean isBackupReminderEnabled() {
|
||||
return _prefs.getBoolean("pref_backup_reminder", true);
|
||||
}
|
||||
|
||||
public void setIsBackupReminderEnabled(boolean enabled) {
|
||||
_prefs.edit().putBoolean("pref_backup_reminder", enabled).apply();
|
||||
}
|
||||
|
||||
public Uri getBackupsLocation() {
|
||||
String str = _prefs.getString("pref_backups_location", null);
|
||||
if (str != null) {
|
||||
return Uri.parse(str);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean getFocusSearchEnabled() {
|
||||
return _prefs.getBoolean("pref_focus_search", false);
|
||||
}
|
||||
|
||||
public void setFocusSearch(boolean enabled) {
|
||||
_prefs.edit().putBoolean("pref_focus_search", enabled).apply();
|
||||
}
|
||||
|
||||
public void setLatestExportTimeNow() {
|
||||
_prefs.edit().putLong("pref_export_latest", new Date().getTime()).apply();
|
||||
setIsBackupReminderNeeded(false);
|
||||
}
|
||||
|
||||
public Date getLatestBackupOrExportTime() {
|
||||
List<Date> dates = new ArrayList<>();
|
||||
|
||||
long l = _prefs.getLong("pref_export_latest", 0);
|
||||
if (l > 0) {
|
||||
dates.add(new Date(l));
|
||||
}
|
||||
|
||||
BackupResult builtinRes = getBuiltInBackupResult();
|
||||
if (builtinRes != null) {
|
||||
dates.add(builtinRes.getTime());
|
||||
}
|
||||
|
||||
BackupResult androidRes = getAndroidBackupResult();
|
||||
if (androidRes != null) {
|
||||
dates.add(androidRes.getTime());
|
||||
}
|
||||
|
||||
if (dates.size() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Collections.max(dates, Date::compareTo);
|
||||
}
|
||||
|
||||
public void setBackupsLocation(Uri location) {
|
||||
_prefs.edit().putString("pref_backups_location", location == null ? null : location.toString()).apply();
|
||||
}
|
||||
|
||||
public int getBackupsVersionCount() {
|
||||
return _prefs.getInt("pref_backups_versions", 5);
|
||||
}
|
||||
|
||||
public void setBackupsVersionCount(int versions) {
|
||||
_prefs.edit().putInt("pref_backups_versions", versions).apply();
|
||||
}
|
||||
|
||||
public void setAndroidBackupResult(@Nullable BackupResult res) {
|
||||
setBackupResult(false, res);
|
||||
}
|
||||
|
||||
public void setBuiltInBackupResult(@Nullable BackupResult res) {
|
||||
setBackupResult(true, res);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public BackupResult getAndroidBackupResult() {
|
||||
return getBackupResult(false);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public BackupResult getBuiltInBackupResult() {
|
||||
return getBackupResult(true);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Preferences.BackupResult getErroredBackupResult() {
|
||||
Preferences.BackupResult res = getBuiltInBackupResult();
|
||||
if (res != null && !res.isSuccessful()) {
|
||||
return res;
|
||||
}
|
||||
res = getAndroidBackupResult();
|
||||
if (res != null && !res.isSuccessful()) {
|
||||
return res;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void setBackupResult(boolean isBuiltInBackup, @Nullable BackupResult res) {
|
||||
String json = null;
|
||||
if (res != null) {
|
||||
res.setIsBuiltIn(isBuiltInBackup);
|
||||
json = res.toJson();
|
||||
}
|
||||
_prefs.edit().putString(getBackupResultKey(isBuiltInBackup), json).apply();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private BackupResult getBackupResult(boolean isBuiltInBackup) {
|
||||
String json = _prefs.getString(getBackupResultKey(isBuiltInBackup), null);
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
BackupResult res = BackupResult.fromJson(json);
|
||||
res.setIsBuiltIn(isBuiltInBackup);
|
||||
return res;
|
||||
} catch (JSONException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getBackupResultKey(boolean isBuiltInBackup) {
|
||||
return isBuiltInBackup ? "pref_backups_result_builtin": "pref_backups_result_android";
|
||||
}
|
||||
|
||||
public void setIsBackupReminderNeeded(boolean needed) {
|
||||
if (isBackupsReminderNeeded() != needed) {
|
||||
_prefs.edit().putBoolean("pref_backups_reminder_needed", needed).apply();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isBackupsReminderNeeded() {
|
||||
return _prefs.getBoolean("pref_backups_reminder_needed", false);
|
||||
}
|
||||
|
||||
public void setIsPlaintextBackupWarningNeeded(boolean needed) {
|
||||
_prefs.edit().putBoolean("pref_plaintext_backup_warning_needed", needed).apply();
|
||||
}
|
||||
|
||||
public boolean isPlaintextBackupWarningNeeded() {
|
||||
return !isPlaintextBackupWarningDisabled()
|
||||
&& _prefs.getBoolean("pref_plaintext_backup_warning_needed", false);
|
||||
}
|
||||
|
||||
public void setIsPlaintextBackupWarningDisabled(boolean disabled) {
|
||||
_prefs.edit().putBoolean("pref_plaintext_backup_warning_disabled", disabled).apply();
|
||||
}
|
||||
|
||||
public boolean isPlaintextBackupWarningDisabled() {
|
||||
return _prefs.getBoolean("pref_plaintext_backup_warning_disabled", false);
|
||||
}
|
||||
|
||||
public boolean isPinKeyboardEnabled() {
|
||||
return _prefs.getBoolean("pref_pin_keyboard", false);
|
||||
}
|
||||
|
||||
public boolean isTimeSyncWarningEnabled() {
|
||||
return _prefs.getBoolean("pref_warn_time_sync", true);
|
||||
}
|
||||
|
||||
public void setIsTimeSyncWarningEnabled(boolean enabled) {
|
||||
_prefs.edit().putBoolean("pref_warn_time_sync", enabled).apply();
|
||||
}
|
||||
|
||||
public CopyBehavior getCopyBehavior() {
|
||||
return CopyBehavior.fromInteger(_prefs.getInt("pref_current_copy_behavior", 0));
|
||||
}
|
||||
|
||||
public void setCopyBehavior(CopyBehavior copyBehavior) {
|
||||
_prefs.edit().putInt("pref_current_copy_behavior", copyBehavior.ordinal()).apply();
|
||||
}
|
||||
|
||||
public boolean isMinimizeOnCopyEnabled() {
|
||||
return _prefs.getBoolean("pref_minimize_on_copy", false);
|
||||
}
|
||||
|
||||
public void setGroupFilter(Set<UUID> groupFilter) {
|
||||
JSONArray json = new JSONArray(groupFilter);
|
||||
_prefs.edit().putString("pref_group_filter_uuids", json.toString()).apply();
|
||||
}
|
||||
|
||||
public Set<UUID> getGroupFilter() {
|
||||
String raw = _prefs.getString("pref_group_filter_uuids", null);
|
||||
if (raw == null || raw.isEmpty()) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
try {
|
||||
JSONArray json = new JSONArray(raw);
|
||||
Set<UUID> filter = new HashSet<>();
|
||||
for (int i = 0; i < json.length(); i++) {
|
||||
filter.add(json.isNull(i) ? null : UUID.fromString(json.getString(i)));
|
||||
}
|
||||
return filter;
|
||||
} catch (JSONException e) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public BackupsVersioningStrategy getBackupVersioningStrategy() {
|
||||
Uri uri = getBackupsLocation();
|
||||
if (uri == null) {
|
||||
return BackupsVersioningStrategy.UNDEFINED;
|
||||
}
|
||||
if (DocumentsContractCompat.isTreeUri(uri)) {
|
||||
return BackupsVersioningStrategy.MULTIPLE_BACKUPS;
|
||||
} else {
|
||||
return BackupsVersioningStrategy.SINGLE_BACKUP;
|
||||
}
|
||||
}
|
||||
|
||||
public static class BackupResult {
|
||||
private final Date _time;
|
||||
private boolean _isBuiltIn;
|
||||
private final String _error;
|
||||
private final boolean _isPermissionError;
|
||||
|
||||
public BackupResult(@Nullable Exception e) {
|
||||
this(new Date(), e == null ? null : e.toString(), e instanceof VaultBackupPermissionException);
|
||||
}
|
||||
|
||||
private BackupResult(Date time, @Nullable String error, boolean isPermissionError) {
|
||||
_time = time;
|
||||
_error = error;
|
||||
_isPermissionError = isPermissionError;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getError() {
|
||||
return _error;
|
||||
}
|
||||
|
||||
public boolean isSuccessful() {
|
||||
return _error == null;
|
||||
}
|
||||
|
||||
public Date getTime() {
|
||||
return _time;
|
||||
}
|
||||
|
||||
public String getElapsedSince(Context context) {
|
||||
return TimeUtils.getElapsedSince(context, _time);
|
||||
}
|
||||
|
||||
public boolean isBuiltIn() {
|
||||
return _isBuiltIn;
|
||||
}
|
||||
|
||||
private void setIsBuiltIn(boolean isBuiltIn) {
|
||||
_isBuiltIn = isBuiltIn;
|
||||
}
|
||||
|
||||
public boolean isPermissionError() {
|
||||
return _isPermissionError;
|
||||
}
|
||||
|
||||
public String toJson() {
|
||||
JSONObject obj = new JSONObject();
|
||||
|
||||
try {
|
||||
obj.put("time", _time.getTime());
|
||||
obj.put("error", _error == null ? JSONObject.NULL : _error);
|
||||
obj.put("isPermissionError", _isPermissionError);
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return obj.toString();
|
||||
}
|
||||
|
||||
public static BackupResult fromJson(String json) throws JSONException {
|
||||
JSONObject obj = new JSONObject(json);
|
||||
long time = obj.getLong("time");
|
||||
String error = JsonUtils.optString(obj, "error");
|
||||
boolean isPermissionError = obj.optBoolean("isPermissionError");
|
||||
return new BackupResult(new Date(time), error, isPermissionError);
|
||||
}
|
||||
}
|
||||
|
||||
public enum CodeGrouping {
|
||||
HALVES(-1),
|
||||
NO_GROUPING(-2),
|
||||
GROUPING_TWOS(2),
|
||||
GROUPING_THREES(3),
|
||||
GROUPING_FOURS(4);
|
||||
|
||||
private final int _value;
|
||||
CodeGrouping(int value) {
|
||||
_value = value;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return _value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import com.beemdevelopment.aegis.helpers.comparators.LastUsedComparator;
|
||||
import com.beemdevelopment.aegis.helpers.comparators.UsageCountComparator;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.helpers.comparators.AccountNameComparator;
|
||||
import com.beemdevelopment.aegis.helpers.comparators.IssuerNameComparator;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
|
||||
public enum SortCategory {
|
||||
CUSTOM,
|
||||
ACCOUNT,
|
||||
ACCOUNT_REVERSED,
|
||||
ISSUER,
|
||||
ISSUER_REVERSED,
|
||||
USAGE_COUNT,
|
||||
LAST_USED;
|
||||
|
||||
private static SortCategory[] _values;
|
||||
|
||||
static {
|
||||
_values = values();
|
||||
}
|
||||
|
||||
public static SortCategory fromInteger(int x) {
|
||||
return _values[x];
|
||||
}
|
||||
|
||||
public Comparator<VaultEntry> getComparator() {
|
||||
Comparator<VaultEntry> comparator = null;
|
||||
|
||||
switch (this) {
|
||||
case ACCOUNT:
|
||||
comparator = new AccountNameComparator().thenComparing(new IssuerNameComparator());
|
||||
break;
|
||||
case ACCOUNT_REVERSED:
|
||||
comparator = Collections.reverseOrder(new AccountNameComparator().thenComparing(new IssuerNameComparator()));
|
||||
break;
|
||||
case ISSUER:
|
||||
comparator = new IssuerNameComparator().thenComparing(new AccountNameComparator());
|
||||
break;
|
||||
case ISSUER_REVERSED:
|
||||
comparator = Collections.reverseOrder(new IssuerNameComparator().thenComparing(new AccountNameComparator()));
|
||||
break;
|
||||
case USAGE_COUNT:
|
||||
comparator = Collections.reverseOrder(new UsageCountComparator());
|
||||
break;
|
||||
case LAST_USED:
|
||||
comparator = Collections.reverseOrder(new LastUsedComparator());
|
||||
}
|
||||
|
||||
return comparator;
|
||||
}
|
||||
|
||||
public int getMenuItem() {
|
||||
switch (this) {
|
||||
case CUSTOM:
|
||||
return R.id.menu_sort_custom;
|
||||
case ACCOUNT:
|
||||
return R.id.menu_sort_alphabetically_name;
|
||||
case ACCOUNT_REVERSED:
|
||||
return R.id.menu_sort_alphabetically_name_reverse;
|
||||
case ISSUER:
|
||||
return R.id.menu_sort_alphabetically;
|
||||
case ISSUER_REVERSED:
|
||||
return R.id.menu_sort_alphabetically_reverse;
|
||||
case USAGE_COUNT:
|
||||
return R.id.menu_sort_usage_count;
|
||||
case LAST_USED:
|
||||
return R.id.menu_sort_last_used;
|
||||
default:
|
||||
return R.id.menu_sort_custom;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
app/src/main/java/com/beemdevelopment/aegis/Theme.java
Normal file
19
app/src/main/java/com/beemdevelopment/aegis/Theme.java
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
public enum Theme {
|
||||
LIGHT,
|
||||
DARK,
|
||||
AMOLED,
|
||||
SYSTEM,
|
||||
SYSTEM_AMOLED;
|
||||
|
||||
private static Theme[] _values;
|
||||
|
||||
static {
|
||||
_values = values();
|
||||
}
|
||||
|
||||
public static Theme fromInteger(int x) {
|
||||
return _values[x];
|
||||
}
|
||||
}
|
||||
17
app/src/main/java/com/beemdevelopment/aegis/ThemeMap.java
Normal file
17
app/src/main/java/com/beemdevelopment/aegis/ThemeMap.java
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ThemeMap {
|
||||
private ThemeMap() {
|
||||
|
||||
}
|
||||
|
||||
public static final Map<Theme, Integer> DEFAULT = ImmutableMap.of(
|
||||
Theme.LIGHT, R.style.Theme_Aegis_Light,
|
||||
Theme.DARK, R.style.Theme_Aegis_Dark,
|
||||
Theme.AMOLED, R.style.Theme_Aegis_Amoled
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class VibrationPatterns {
|
||||
public static final long[] EXPIRING = {475, 20, 5, 20, 965, 20, 5, 20, 965, 20, 5, 20, 420};
|
||||
public static final long[] REFRESH_CODE = {0, 100};
|
||||
|
||||
public static long getLengthInMillis(long[] pattern) {
|
||||
return Arrays.stream(pattern).sum();
|
||||
}
|
||||
}
|
||||
65
app/src/main/java/com/beemdevelopment/aegis/ViewMode.java
Normal file
65
app/src/main/java/com/beemdevelopment/aegis/ViewMode.java
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
|
||||
public enum ViewMode {
|
||||
NORMAL,
|
||||
COMPACT,
|
||||
SMALL,
|
||||
TILES;
|
||||
|
||||
private static ViewMode[] _values;
|
||||
|
||||
static {
|
||||
_values = values();
|
||||
}
|
||||
|
||||
public static ViewMode fromInteger(int x) {
|
||||
return _values[x];
|
||||
}
|
||||
|
||||
@LayoutRes
|
||||
public int getLayoutId() {
|
||||
switch (this) {
|
||||
case NORMAL:
|
||||
return R.layout.card_entry;
|
||||
case COMPACT:
|
||||
return R.layout.card_entry_compact;
|
||||
case SMALL:
|
||||
return R.layout.card_entry_small;
|
||||
case TILES:
|
||||
return R.layout.card_entry_tile;
|
||||
default:
|
||||
return R.layout.card_entry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the offset (in dp) that should exist between entries in this view mode.
|
||||
*/
|
||||
public float getItemOffset() {
|
||||
if (this == ViewMode.COMPACT) {
|
||||
return 1;
|
||||
} else if (this == ViewMode.TILES) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
return 8;
|
||||
}
|
||||
|
||||
public int getSpanCount() {
|
||||
if (this == ViewMode.TILES) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
public String getFormattedAccountName(String accountName) {
|
||||
if (this == ViewMode.TILES) {
|
||||
return accountName;
|
||||
}
|
||||
|
||||
return String.format("(%s)", accountName);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class CryptParameters implements Serializable {
|
||||
private byte[] _nonce;
|
||||
private byte[] _tag;
|
||||
|
||||
public CryptParameters(byte[] nonce, byte[] tag) {
|
||||
_nonce = nonce;
|
||||
_tag = tag;
|
||||
}
|
||||
|
||||
public JSONObject toJson() {
|
||||
JSONObject obj = new JSONObject();
|
||||
|
||||
try {
|
||||
obj.put("nonce", Hex.encode(_nonce));
|
||||
obj.put("tag", Hex.encode(_tag));
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
public static CryptParameters fromJson(JSONObject obj) throws JSONException, EncodingException {
|
||||
byte[] nonce = Hex.decode(obj.getString("nonce"));
|
||||
byte[] tag = Hex.decode(obj.getString("tag"));
|
||||
return new CryptParameters(nonce, tag);
|
||||
}
|
||||
|
||||
public byte[] getNonce() {
|
||||
return _nonce;
|
||||
}
|
||||
|
||||
public byte[] getTag() {
|
||||
return _tag;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
public class CryptResult {
|
||||
private byte[] _data;
|
||||
private CryptParameters _params;
|
||||
|
||||
public CryptResult(byte[] data, CryptParameters params) {
|
||||
_data = data;
|
||||
_params = params;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return _data;
|
||||
}
|
||||
|
||||
public CryptParameters getParams() {
|
||||
return _params;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.bc.SCrypt;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class CryptoUtils {
|
||||
public static final String CRYPTO_AEAD = "AES/GCM/NoPadding";
|
||||
public static final byte CRYPTO_AEAD_KEY_SIZE = 32;
|
||||
public static final byte CRYPTO_AEAD_TAG_SIZE = 16;
|
||||
public static final byte CRYPTO_AEAD_NONCE_SIZE = 12;
|
||||
|
||||
public static final int CRYPTO_SCRYPT_N = 1 << 15;
|
||||
public static final int CRYPTO_SCRYPT_r = 8;
|
||||
public static final int CRYPTO_SCRYPT_p = 1;
|
||||
|
||||
public static SecretKey deriveKey(byte[] input, SCryptParameters params) {
|
||||
byte[] keyBytes = SCrypt.generate(input, params.getSalt(), params.getN(), params.getR(), params.getP(), CRYPTO_AEAD_KEY_SIZE);
|
||||
return new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
|
||||
}
|
||||
|
||||
public static SecretKey deriveKey(char[] password, SCryptParameters params) {
|
||||
byte[] bytes = toBytes(password);
|
||||
return deriveKey(bytes, params);
|
||||
}
|
||||
|
||||
public static Cipher createEncryptCipher(SecretKey key)
|
||||
throws NoSuchPaddingException, NoSuchAlgorithmException,
|
||||
InvalidAlgorithmParameterException, InvalidKeyException {
|
||||
return createCipher(key, Cipher.ENCRYPT_MODE, null);
|
||||
}
|
||||
|
||||
public static Cipher createDecryptCipher(SecretKey key, byte[] nonce)
|
||||
throws InvalidAlgorithmParameterException, NoSuchAlgorithmException,
|
||||
InvalidKeyException, NoSuchPaddingException {
|
||||
return createCipher(key, Cipher.DECRYPT_MODE, nonce);
|
||||
}
|
||||
|
||||
private static Cipher createCipher(SecretKey key, int opmode, byte[] nonce)
|
||||
throws NoSuchPaddingException, NoSuchAlgorithmException,
|
||||
InvalidAlgorithmParameterException, InvalidKeyException {
|
||||
Cipher cipher = Cipher.getInstance(CRYPTO_AEAD);
|
||||
|
||||
// generate the nonce if none is given
|
||||
// we are not allowed to do this ourselves as "setRandomizedEncryptionRequired" is set to true
|
||||
if (nonce != null) {
|
||||
AlgorithmParameterSpec spec = new GCMParameterSpec(CRYPTO_AEAD_TAG_SIZE * 8, nonce);
|
||||
cipher.init(opmode, key, spec);
|
||||
} else {
|
||||
cipher.init(opmode, key);
|
||||
}
|
||||
|
||||
return cipher;
|
||||
}
|
||||
|
||||
public static CryptResult encrypt(byte[] data, Cipher cipher)
|
||||
throws BadPaddingException, IllegalBlockSizeException {
|
||||
// split off the tag to store it separately
|
||||
byte[] result = cipher.doFinal(data);
|
||||
byte[] tag = Arrays.copyOfRange(result, result.length - CRYPTO_AEAD_TAG_SIZE, result.length);
|
||||
byte[] encrypted = Arrays.copyOfRange(result, 0, result.length - CRYPTO_AEAD_TAG_SIZE);
|
||||
|
||||
return new CryptResult(encrypted, new CryptParameters(cipher.getIV(), tag));
|
||||
}
|
||||
|
||||
public static CryptResult decrypt(byte[] encrypted, Cipher cipher, CryptParameters params)
|
||||
throws IOException, BadPaddingException, IllegalBlockSizeException {
|
||||
return decrypt(encrypted, 0, encrypted.length, cipher, params);
|
||||
}
|
||||
|
||||
public static CryptResult decrypt(byte[] encrypted, int encryptedOffset, int encryptedLen, Cipher cipher, CryptParameters params)
|
||||
throws IOException, BadPaddingException, IllegalBlockSizeException {
|
||||
// append the tag to the ciphertext
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
stream.write(encrypted, encryptedOffset, encryptedLen);
|
||||
stream.write(params.getTag());
|
||||
|
||||
encrypted = stream.toByteArray();
|
||||
byte[] decrypted = cipher.doFinal(encrypted);
|
||||
|
||||
return new CryptResult(decrypted, params);
|
||||
}
|
||||
|
||||
public static SecretKey generateKey() {
|
||||
try {
|
||||
KeyGenerator generator = KeyGenerator.getInstance("AES");
|
||||
generator.init(CRYPTO_AEAD_KEY_SIZE * 8);
|
||||
return generator.generateKey();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] generateSalt() {
|
||||
return generateRandomBytes(CRYPTO_AEAD_KEY_SIZE);
|
||||
}
|
||||
|
||||
public static byte[] generateRandomBytes(int length) {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] data = new byte[length];
|
||||
random.nextBytes(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
public static byte[] toBytes(char[] chars) {
|
||||
CharBuffer charBuf = CharBuffer.wrap(chars);
|
||||
ByteBuffer byteBuf = StandardCharsets.UTF_8.encode(charBuf);
|
||||
byte[] bytes = new byte[byteBuf.limit()];
|
||||
byteBuf.get(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static byte[] toBytesOld(char[] chars) {
|
||||
CharBuffer charBuf = CharBuffer.wrap(chars);
|
||||
ByteBuffer byteBuf = StandardCharsets.UTF_8.encode(charBuf);
|
||||
return byteBuf.array();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyProperties;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.ProviderException;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.Collections;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class KeyStoreHandle {
|
||||
private final KeyStore _keyStore;
|
||||
private static final String STORE_NAME = "AndroidKeyStore";
|
||||
|
||||
public KeyStoreHandle() throws KeyStoreHandleException {
|
||||
try {
|
||||
_keyStore = KeyStore.getInstance(STORE_NAME);
|
||||
_keyStore.load(null);
|
||||
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) {
|
||||
throw new KeyStoreHandleException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean containsKey(String id) throws KeyStoreHandleException {
|
||||
try {
|
||||
return _keyStore.containsAlias(id);
|
||||
} catch (KeyStoreException e) {
|
||||
throw new KeyStoreHandleException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public SecretKey generateKey(String id) throws KeyStoreHandleException {
|
||||
try {
|
||||
KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, STORE_NAME);
|
||||
generator.init(new KeyGenParameterSpec.Builder(id,
|
||||
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
.setUserAuthenticationRequired(true)
|
||||
.setRandomizedEncryptionRequired(true)
|
||||
.setKeySize(CryptoUtils.CRYPTO_AEAD_KEY_SIZE * 8)
|
||||
.build());
|
||||
|
||||
return generator.generateKey();
|
||||
} catch (ProviderException e) {
|
||||
// a ProviderException can occur at runtime with buggy Keymaster HAL implementations
|
||||
// so if this was caused by an android.security.KeyStoreException, throw a KeyStoreHandleException instead
|
||||
Throwable cause = e.getCause();
|
||||
if (cause != null && cause.getClass().getName().equals("android.security.KeyStoreException")) {
|
||||
throw new KeyStoreHandleException(cause);
|
||||
}
|
||||
throw e;
|
||||
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
|
||||
throw new KeyStoreHandleException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public SecretKey getKey(String id) throws KeyStoreHandleException {
|
||||
SecretKey key;
|
||||
|
||||
try {
|
||||
key = (SecretKey) _keyStore.getKey(id, null);
|
||||
} catch (UnrecoverableKeyException e) {
|
||||
return null;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (KeyStoreException e) {
|
||||
throw new KeyStoreHandleException(e);
|
||||
}
|
||||
|
||||
if (isKeyPermanentlyInvalidated(key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
private static boolean isKeyPermanentlyInvalidated(SecretKey key) {
|
||||
// try to initialize a dummy cipher and see if an InvalidKeyException is thrown
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_AEAD);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
} catch (InvalidKeyException e) {
|
||||
// some devices throw a plain InvalidKeyException, not KeyPermanentlyInvalidatedException
|
||||
return true;
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void deleteKey(String id) throws KeyStoreHandleException {
|
||||
try {
|
||||
_keyStore.deleteEntry(id);
|
||||
} catch (KeyStoreException e) {
|
||||
throw new KeyStoreHandleException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() throws KeyStoreHandleException {
|
||||
try {
|
||||
for (String alias : Collections.list(_keyStore.aliases())) {
|
||||
deleteKey(alias);
|
||||
}
|
||||
} catch (KeyStoreException e) {
|
||||
throw new KeyStoreHandleException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
public class KeyStoreHandleException extends Exception {
|
||||
public KeyStoreHandleException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public KeyStoreHandleException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class MasterKey implements Serializable {
|
||||
private SecretKey _key;
|
||||
|
||||
public MasterKey(SecretKey key) {
|
||||
if (key == null) {
|
||||
throw new IllegalArgumentException("Key cannot be null");
|
||||
}
|
||||
_key = key;
|
||||
}
|
||||
|
||||
public static MasterKey generate() {
|
||||
return new MasterKey(CryptoUtils.generateKey());
|
||||
}
|
||||
|
||||
public CryptResult encrypt(byte[] bytes) throws MasterKeyException {
|
||||
try {
|
||||
Cipher cipher = CryptoUtils.createEncryptCipher(_key);
|
||||
return CryptoUtils.encrypt(bytes, cipher);
|
||||
} catch (NoSuchPaddingException
|
||||
| NoSuchAlgorithmException
|
||||
| InvalidAlgorithmParameterException
|
||||
| InvalidKeyException
|
||||
| BadPaddingException
|
||||
| IllegalBlockSizeException e) {
|
||||
throw new MasterKeyException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public CryptResult decrypt(byte[] bytes, CryptParameters params) throws MasterKeyException {
|
||||
try {
|
||||
Cipher cipher = CryptoUtils.createDecryptCipher(_key, params.getNonce());
|
||||
return CryptoUtils.decrypt(bytes, cipher, params);
|
||||
} catch (NoSuchPaddingException
|
||||
| NoSuchAlgorithmException
|
||||
| InvalidAlgorithmParameterException
|
||||
| InvalidKeyException
|
||||
| BadPaddingException
|
||||
| IOException
|
||||
| IllegalBlockSizeException e) {
|
||||
throw new MasterKeyException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
return _key.getEncoded();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
public class MasterKeyException extends Exception {
|
||||
public MasterKeyException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class SCryptParameters implements Serializable {
|
||||
private int _n;
|
||||
private int _r;
|
||||
private int _p;
|
||||
private byte[] _salt;
|
||||
|
||||
public SCryptParameters(int n, int r, int p, byte[] salt) {
|
||||
_n = n;
|
||||
_r = r;
|
||||
_p = p;
|
||||
_salt = salt;
|
||||
}
|
||||
|
||||
public byte[] getSalt() {
|
||||
return _salt;
|
||||
}
|
||||
|
||||
public int getN() {
|
||||
return _n;
|
||||
}
|
||||
|
||||
public int getR() {
|
||||
return _r;
|
||||
}
|
||||
|
||||
public int getP() {
|
||||
return _p;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
/*
|
||||
Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
|
||||
|
||||
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 com.beemdevelopment.aegis.crypto.bc;
|
||||
|
||||
import org.bouncycastle.crypto.PBEParametersGenerator;
|
||||
import org.bouncycastle.crypto.digests.SHA256Digest;
|
||||
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
import org.bouncycastle.util.Arrays;
|
||||
import org.bouncycastle.util.Integers;
|
||||
import org.bouncycastle.util.Pack;
|
||||
|
||||
/**
|
||||
* Implementation of the scrypt a password-based key derivation function.
|
||||
* <p>
|
||||
* Scrypt was created by Colin Percival and is specified in <a
|
||||
* href="https://tools.ietf.org/html/rfc7914">RFC 7914 - The scrypt Password-Based Key Derivation Function</a>
|
||||
*/
|
||||
public class SCrypt
|
||||
{
|
||||
private SCrypt()
|
||||
{
|
||||
// not used.
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a key using the scrypt key derivation function.
|
||||
*
|
||||
* @param P the bytes of the pass phrase.
|
||||
* @param S the salt to use for this invocation.
|
||||
* @param N CPU/Memory cost parameter. Must be larger than 1, a power of 2 and less than
|
||||
* <code>2^(128 * r / 8)</code>.
|
||||
* @param r the block size, must be >= 1.
|
||||
* @param p Parallelization parameter. Must be a positive integer less than or equal to
|
||||
* <code>Integer.MAX_VALUE / (128 * r * 8)</code>.
|
||||
* @param dkLen the length of the key to generate.
|
||||
* @return the generated key.
|
||||
*/
|
||||
public static byte[] generate(byte[] P, byte[] S, int N, int r, int p, int dkLen)
|
||||
{
|
||||
if (P == null)
|
||||
{
|
||||
throw new IllegalArgumentException("Passphrase P must be provided.");
|
||||
}
|
||||
if (S == null)
|
||||
{
|
||||
throw new IllegalArgumentException("Salt S must be provided.");
|
||||
}
|
||||
if (N <= 1 || !isPowerOf2(N))
|
||||
{
|
||||
throw new IllegalArgumentException("Cost parameter N must be > 1 and a power of 2");
|
||||
}
|
||||
// Only value of r that cost (as an int) could be exceeded for is 1
|
||||
if (r == 1 && N >= 65536)
|
||||
{
|
||||
throw new IllegalArgumentException("Cost parameter N must be > 1 and < 65536.");
|
||||
}
|
||||
if (r < 1)
|
||||
{
|
||||
throw new IllegalArgumentException("Block size r must be >= 1.");
|
||||
}
|
||||
int maxParallel = Integer.MAX_VALUE / (128 * r * 8);
|
||||
if (p < 1 || p > maxParallel)
|
||||
{
|
||||
throw new IllegalArgumentException("Parallelisation parameter p must be >= 1 and <= " + maxParallel
|
||||
+ " (based on block size r of " + r + ")");
|
||||
}
|
||||
if (dkLen < 1)
|
||||
{
|
||||
throw new IllegalArgumentException("Generated key length dkLen must be >= 1.");
|
||||
}
|
||||
return MFcrypt(P, S, N, r, p, dkLen);
|
||||
}
|
||||
|
||||
private static byte[] MFcrypt(byte[] P, byte[] S, int N, int r, int p, int dkLen)
|
||||
{
|
||||
int MFLenBytes = r * 128;
|
||||
byte[] bytes = SingleIterationPBKDF2(P, S, p * MFLenBytes);
|
||||
|
||||
int[] B = null;
|
||||
|
||||
try
|
||||
{
|
||||
int BLen = bytes.length >>> 2;
|
||||
B = new int[BLen];
|
||||
|
||||
Pack.littleEndianToInt(bytes, 0, B);
|
||||
|
||||
/*
|
||||
* Chunk memory allocations; We choose 'd' so that there will be 2**d chunks, each not
|
||||
* larger than 32KiB, except that the minimum chunk size is 2 * r * 32.
|
||||
*/
|
||||
int d = 0, total = N * r;
|
||||
while ((N - d) > 2 && total > (1 << 10))
|
||||
{
|
||||
++d;
|
||||
total >>>= 1;
|
||||
}
|
||||
|
||||
int MFLenWords = MFLenBytes >>> 2;
|
||||
for (int BOff = 0; BOff < BLen; BOff += MFLenWords)
|
||||
{
|
||||
// TODO These can be done in parallel threads
|
||||
SMix(B, BOff, N, d, r);
|
||||
}
|
||||
|
||||
Pack.intToLittleEndian(B, bytes, 0);
|
||||
|
||||
return SingleIterationPBKDF2(P, bytes, dkLen);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Clear(bytes);
|
||||
Clear(B);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] SingleIterationPBKDF2(byte[] P, byte[] S, int dkLen)
|
||||
{
|
||||
PBEParametersGenerator pGen = new PKCS5S2ParametersGenerator(new SHA256Digest());
|
||||
pGen.init(P, S, 1);
|
||||
KeyParameter key = (KeyParameter)pGen.generateDerivedMacParameters(dkLen * 8);
|
||||
return key.getKey();
|
||||
}
|
||||
|
||||
private static void SMix(int[] B, int BOff, int N, int d, int r)
|
||||
{
|
||||
int powN = Integers.numberOfTrailingZeros(N);
|
||||
int blocksPerChunk = N >>> d;
|
||||
int chunkCount = 1 << d, chunkMask = blocksPerChunk - 1, chunkPow = powN - d;
|
||||
|
||||
int BCount = r * 32;
|
||||
|
||||
int[] blockX1 = new int[16];
|
||||
int[] blockX2 = new int[16];
|
||||
int[] blockY = new int[BCount];
|
||||
|
||||
int[] X = new int[BCount];
|
||||
int[][] VV = new int[chunkCount][];
|
||||
|
||||
try
|
||||
{
|
||||
System.arraycopy(B, BOff, X, 0, BCount);
|
||||
|
||||
for (int c = 0; c < chunkCount; ++c)
|
||||
{
|
||||
int[] V = new int[blocksPerChunk * BCount];
|
||||
VV[c] = V;
|
||||
|
||||
int off = 0;
|
||||
for (int i = 0; i < blocksPerChunk; i += 2)
|
||||
{
|
||||
System.arraycopy(X, 0, V, off, BCount);
|
||||
off += BCount;
|
||||
BlockMix(X, blockX1, blockX2, blockY, r);
|
||||
System.arraycopy(blockY, 0, V, off, BCount);
|
||||
off += BCount;
|
||||
BlockMix(blockY, blockX1, blockX2, X, r);
|
||||
}
|
||||
}
|
||||
|
||||
int mask = N - 1;
|
||||
for (int i = 0; i < N; ++i)
|
||||
{
|
||||
int j = X[BCount - 16] & mask;
|
||||
int[] V = VV[j >>> chunkPow];
|
||||
int VOff = (j & chunkMask) * BCount;
|
||||
System.arraycopy(V, VOff, blockY, 0, BCount);
|
||||
Xor(blockY, X, 0, blockY);
|
||||
BlockMix(blockY, blockX1, blockX2, X, r);
|
||||
}
|
||||
|
||||
System.arraycopy(X, 0, B, BOff, BCount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ClearAll(VV);
|
||||
ClearAll(new int[][]{X, blockX1, blockX2, blockY});
|
||||
}
|
||||
}
|
||||
|
||||
private static void BlockMix(int[] B, int[] X1, int[] X2, int[] Y, int r)
|
||||
{
|
||||
System.arraycopy(B, B.length - 16, X1, 0, 16);
|
||||
|
||||
int BOff = 0, YOff = 0, halfLen = B.length >>> 1;
|
||||
|
||||
for (int i = 2 * r; i > 0; --i)
|
||||
{
|
||||
Xor(X1, B, BOff, X2);
|
||||
|
||||
Salsa20Engine.salsaCore(8, X2, X1);
|
||||
System.arraycopy(X1, 0, Y, YOff, 16);
|
||||
|
||||
YOff = halfLen + BOff - YOff;
|
||||
BOff += 16;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Xor(int[] a, int[] b, int bOff, int[] output)
|
||||
{
|
||||
for (int i = output.length - 1; i >= 0; --i)
|
||||
{
|
||||
output[i] = a[i] ^ b[bOff + i];
|
||||
}
|
||||
}
|
||||
|
||||
private static void Clear(byte[] array)
|
||||
{
|
||||
if (array != null)
|
||||
{
|
||||
Arrays.fill(array, (byte)0);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Clear(int[] array)
|
||||
{
|
||||
if (array != null)
|
||||
{
|
||||
Arrays.fill(array, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ClearAll(int[][] arrays)
|
||||
{
|
||||
for (int i = 0; i < arrays.length; ++i)
|
||||
{
|
||||
Clear(arrays[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// note: we know X is non-zero
|
||||
private static boolean isPowerOf2(int x)
|
||||
{
|
||||
return ((x & (x - 1)) == 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
|
||||
|
||||
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 com.beemdevelopment.aegis.crypto.bc;
|
||||
|
||||
/**
|
||||
* Implementation of Daniel J. Bernstein's Salsa20 stream cipher, Snuffle 2005
|
||||
*/
|
||||
public class Salsa20Engine {
|
||||
private Salsa20Engine()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public static void salsaCore(int rounds, int[] input, int[] x)
|
||||
{
|
||||
if (input.length != 16)
|
||||
{
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
if (x.length != 16)
|
||||
{
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
if (rounds % 2 != 0)
|
||||
{
|
||||
throw new IllegalArgumentException("Number of rounds must be even");
|
||||
}
|
||||
|
||||
int x00 = input[ 0];
|
||||
int x01 = input[ 1];
|
||||
int x02 = input[ 2];
|
||||
int x03 = input[ 3];
|
||||
int x04 = input[ 4];
|
||||
int x05 = input[ 5];
|
||||
int x06 = input[ 6];
|
||||
int x07 = input[ 7];
|
||||
int x08 = input[ 8];
|
||||
int x09 = input[ 9];
|
||||
int x10 = input[10];
|
||||
int x11 = input[11];
|
||||
int x12 = input[12];
|
||||
int x13 = input[13];
|
||||
int x14 = input[14];
|
||||
int x15 = input[15];
|
||||
|
||||
for (int i = rounds; i > 0; i -= 2)
|
||||
{
|
||||
x04 ^= Integer.rotateLeft(x00 + x12, 7);
|
||||
x08 ^= Integer.rotateLeft(x04 + x00, 9);
|
||||
x12 ^= Integer.rotateLeft(x08 + x04, 13);
|
||||
x00 ^= Integer.rotateLeft(x12 + x08, 18);
|
||||
x09 ^= Integer.rotateLeft(x05 + x01, 7);
|
||||
x13 ^= Integer.rotateLeft(x09 + x05, 9);
|
||||
x01 ^= Integer.rotateLeft(x13 + x09, 13);
|
||||
x05 ^= Integer.rotateLeft(x01 + x13, 18);
|
||||
x14 ^= Integer.rotateLeft(x10 + x06, 7);
|
||||
x02 ^= Integer.rotateLeft(x14 + x10, 9);
|
||||
x06 ^= Integer.rotateLeft(x02 + x14, 13);
|
||||
x10 ^= Integer.rotateLeft(x06 + x02, 18);
|
||||
x03 ^= Integer.rotateLeft(x15 + x11, 7);
|
||||
x07 ^= Integer.rotateLeft(x03 + x15, 9);
|
||||
x11 ^= Integer.rotateLeft(x07 + x03, 13);
|
||||
x15 ^= Integer.rotateLeft(x11 + x07, 18);
|
||||
|
||||
x01 ^= Integer.rotateLeft(x00 + x03, 7);
|
||||
x02 ^= Integer.rotateLeft(x01 + x00, 9);
|
||||
x03 ^= Integer.rotateLeft(x02 + x01, 13);
|
||||
x00 ^= Integer.rotateLeft(x03 + x02, 18);
|
||||
x06 ^= Integer.rotateLeft(x05 + x04, 7);
|
||||
x07 ^= Integer.rotateLeft(x06 + x05, 9);
|
||||
x04 ^= Integer.rotateLeft(x07 + x06, 13);
|
||||
x05 ^= Integer.rotateLeft(x04 + x07, 18);
|
||||
x11 ^= Integer.rotateLeft(x10 + x09, 7);
|
||||
x08 ^= Integer.rotateLeft(x11 + x10, 9);
|
||||
x09 ^= Integer.rotateLeft(x08 + x11, 13);
|
||||
x10 ^= Integer.rotateLeft(x09 + x08, 18);
|
||||
x12 ^= Integer.rotateLeft(x15 + x14, 7);
|
||||
x13 ^= Integer.rotateLeft(x12 + x15, 9);
|
||||
x14 ^= Integer.rotateLeft(x13 + x12, 13);
|
||||
x15 ^= Integer.rotateLeft(x14 + x13, 18);
|
||||
}
|
||||
|
||||
x[ 0] = x00 + input[ 0];
|
||||
x[ 1] = x01 + input[ 1];
|
||||
x[ 2] = x02 + input[ 2];
|
||||
x[ 3] = x03 + input[ 3];
|
||||
x[ 4] = x04 + input[ 4];
|
||||
x[ 5] = x05 + input[ 5];
|
||||
x[ 6] = x06 + input[ 6];
|
||||
x[ 7] = x07 + input[ 7];
|
||||
x[ 8] = x08 + input[ 8];
|
||||
x[ 9] = x09 + input[ 9];
|
||||
x[10] = x10 + input[10];
|
||||
x[11] = x11 + input[11];
|
||||
x[12] = x12 + input[12];
|
||||
x[13] = x13 + input[13];
|
||||
x[14] = x14 + input[14];
|
||||
x[15] = x15 + input[15];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.beemdevelopment.aegis.crypto.otp;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class HOTP {
|
||||
private HOTP() {
|
||||
}
|
||||
|
||||
public static OTP generateOTP(byte[] secret, String algo, int digits, long counter)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
byte[] hash = getHash(secret, algo, counter);
|
||||
|
||||
// truncate hash to get the HTOP value
|
||||
// http://tools.ietf.org/html/rfc4226#section-5.4
|
||||
int offset = hash[hash.length - 1] & 0xf;
|
||||
int otp = ((hash[offset] & 0x7f) << 24)
|
||||
| ((hash[offset + 1] & 0xff) << 16)
|
||||
| ((hash[offset + 2] & 0xff) << 8)
|
||||
| (hash[offset + 3] & 0xff);
|
||||
|
||||
return new OTP(otp, digits);
|
||||
}
|
||||
|
||||
public static byte[] getHash(byte[] secret, String algo, long counter)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
SecretKeySpec key = new SecretKeySpec(secret, "RAW");
|
||||
|
||||
// encode counter in big endian
|
||||
byte[] counterBytes = ByteBuffer.allocate(8)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
.putLong(counter)
|
||||
.array();
|
||||
|
||||
// calculate the hash of the counter
|
||||
Mac mac = Mac.getInstance(algo);
|
||||
mac.init(key);
|
||||
return mac.doFinal(counterBytes);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.beemdevelopment.aegis.crypto.otp;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public class MOTP {
|
||||
private final String _code;
|
||||
private final int _digits;
|
||||
|
||||
private MOTP(String code, int digits) {
|
||||
_code = code;
|
||||
_digits = digits;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static MOTP generateOTP(byte[] secret, String algo, int digits, int period, String pin)
|
||||
throws NoSuchAlgorithmException {
|
||||
|
||||
return generateOTP(secret, algo, digits, period, pin, System.currentTimeMillis() / 1000);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static MOTP generateOTP(byte[] secret, String algo, int digits, int period, String pin, long time)
|
||||
throws NoSuchAlgorithmException {
|
||||
|
||||
long timeBasedCounter = time / period;
|
||||
String secretAsString = Hex.encode(secret);
|
||||
String toDigest = timeBasedCounter + secretAsString + pin;
|
||||
String code = getDigest(algo, toDigest.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
return new MOTP(code, digits);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@NonNull
|
||||
protected static String getDigest(String algo, byte[] toDigest) throws NoSuchAlgorithmException {
|
||||
MessageDigest md = MessageDigest.getInstance(algo);
|
||||
byte[] digest = md.digest(toDigest);
|
||||
|
||||
return Hex.encode(digest);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return _code.substring(0, _digits);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.beemdevelopment.aegis.crypto.otp;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class OTP {
|
||||
private static final String STEAM_ALPHABET = "23456789BCDFGHJKMNPQRTVWXY";
|
||||
|
||||
private final int _code;
|
||||
private final int _digits;
|
||||
|
||||
public OTP(int code, int digits) {
|
||||
_code = code;
|
||||
_digits = digits;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return _code;
|
||||
}
|
||||
|
||||
public int getDigits() {
|
||||
return _digits;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
int code = _code % (int) Math.pow(10, _digits);
|
||||
|
||||
// prepend zeroes if needed
|
||||
StringBuilder res = new StringBuilder(Long.toString(code));
|
||||
while (res.length() < _digits) {
|
||||
res.insert(0, "0");
|
||||
}
|
||||
|
||||
return res.toString();
|
||||
}
|
||||
|
||||
public String toSteamString() {
|
||||
int code = _code;
|
||||
StringBuilder res = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < _digits; i++) {
|
||||
char c = STEAM_ALPHABET.charAt(code % STEAM_ALPHABET.length());
|
||||
res.append(c);
|
||||
code /= STEAM_ALPHABET.length();
|
||||
}
|
||||
|
||||
return res.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.beemdevelopment.aegis.crypto.otp;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public class TOTP {
|
||||
|
||||
private TOTP() {
|
||||
}
|
||||
|
||||
public static OTP generateOTP(byte[] secret, String algo, int digits, long period, long seconds)
|
||||
throws InvalidKeyException, NoSuchAlgorithmException {
|
||||
long counter = (long) Math.floor((double) seconds / period);
|
||||
return HOTP.generateOTP(secret, algo, digits, counter);
|
||||
}
|
||||
|
||||
public static OTP generateOTP(byte[] secret, String algo, int digits, long period)
|
||||
throws InvalidKeyException, NoSuchAlgorithmException {
|
||||
return generateOTP(secret, algo, digits, period, System.currentTimeMillis() / 1000);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package com.beemdevelopment.aegis.crypto.otp;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class YAOTP {
|
||||
private static final int EN_ALPHABET_LENGTH = 26;
|
||||
private final long _code;
|
||||
private final int _digits;
|
||||
|
||||
private YAOTP(long code, int digits) {
|
||||
_code = code;
|
||||
_digits = digits;
|
||||
}
|
||||
|
||||
public static YAOTP generateOTP(byte[] secret, String pin, int digits, String otpAlgo, long period)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException, IOException {
|
||||
long seconds = System.currentTimeMillis() / 1000;
|
||||
return generateOTP(secret, pin, digits, otpAlgo, period, seconds);
|
||||
}
|
||||
|
||||
public static YAOTP generateOTP(byte[] secret, String pin, int digits, String otpAlgo, long period, long seconds)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException, IOException {
|
||||
byte[] pinWithHash;
|
||||
byte[] pinBytes = pin.getBytes(StandardCharsets.UTF_8);
|
||||
try (ByteArrayOutputStream stream = new ByteArrayOutputStream(pinBytes.length + secret.length)) {
|
||||
stream.write(pinBytes);
|
||||
stream.write(secret);
|
||||
pinWithHash = stream.toByteArray();
|
||||
}
|
||||
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
byte[] keyHash = md.digest(pinWithHash);
|
||||
if (keyHash[0] == 0) {
|
||||
keyHash = Arrays.copyOfRange(keyHash, 1, keyHash.length);
|
||||
}
|
||||
|
||||
long counter = (long) Math.floor((double) seconds / period);
|
||||
byte[] periodHash = HOTP.getHash(keyHash, otpAlgo, counter);
|
||||
int offset = periodHash[periodHash.length - 1] & 0xf;
|
||||
periodHash[offset] &= 0x7f;
|
||||
long otp = ByteBuffer.wrap(periodHash)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
.getLong(offset);
|
||||
|
||||
return new YAOTP(otp, digits);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
long code = _code % (long) Math.pow(EN_ALPHABET_LENGTH, _digits);
|
||||
char[] chars = new char[_digits];
|
||||
|
||||
for (int i = _digits - 1; i >= 0; i--) {
|
||||
chars[i] = (char) ('a' + (code % EN_ALPHABET_LENGTH));
|
||||
code /= EN_ALPHABET_LENGTH;
|
||||
}
|
||||
|
||||
return new String(chars);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.beemdevelopment.aegis.crypto.pins;
|
||||
|
||||
import info.guardianproject.trustedintents.ApkSignaturePin;
|
||||
|
||||
public final class GuardianProjectFDroidRSA2048 extends ApkSignaturePin {
|
||||
|
||||
public GuardianProjectFDroidRSA2048() {
|
||||
fingerprints = new String[]{
|
||||
"927f7e38b6acbecd84e02dace33efa9a7a2f0979750f28f585688ee38b3a4e28",
|
||||
};
|
||||
certificates = new byte[][]{
|
||||
{48, -126, 3, 95, 48, -126, 2, 71, -96, 3, 2, 1, 2, 2, 4, 28, -30, 107, -102, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, 30, 23, 13, 49, 55, 49, 50, 48, 55, 49, 55, 51, 48, 52, 50, 90, 23, 13, 52, 53, 48, 52, 50, 52, 49, 55, 51, 48, 52, 50, 90, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, -126, 1, 34, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 1, 5, 0, 3, -126, 1, 15, 0, 48, -126, 1, 10, 2, -126, 1, 1, 0, -107, -115, -106, 1, -26, 72, -105, -99, 62, 3, -55, 34, 99, -112, -68, -20, -115, 31, 34, 118, -50, 12, -32, -59, 74, -58, -37, -87, 21, 105, 36, -82, 13, -51, 66, 4, 55, -111, 13, -46, -7, -69, -15, 36, 118, -7, 101, -86, 123, -83, -103, 110, 116, -54, 112, 46, 12, 96, -76, -48, -70, -33, -81, 52, 59, 73, 107, -126, -72, -25, 32, 93, 29, -20, 5, -41, -27, 123, -9, 104, -31, -59, -1, -83, -93, 99, 85, -116, -62, -55, 18, -63, 6, -51, -110, 33, 9, 7, -49, 102, -20, -122, -124, -68, 93, -102, 31, 48, 86, 96, -99, 105, -52, 95, 12, 57, 99, 12, -24, 70, 40, -99, -20, -21, -85, -70, -105, 95, 117, -31, 126, -126, -39, 46, -62, 59, -23, -74, 108, -12, -56, -40, -96, 79, -37, -82, 1, 99, -104, 48, -60, 92, 14, 109, 127, -22, 31, 115, -27, 108, 9, 92, 118, -45, 103, 117, 57, -50, -82, 114, -113, 68, -82, 87, 96, 111, 72, 65, -63, 12, 31, -34, -31, -55, -101, 101, 101, 59, 73, -119, -122, 82, 28, 47, -108, -85, 59, 46, 89, -93, -1, 9, -11, -51, 63, -44, 109, -76, -103, -26, -49, -80, 6, 52, -27, 73, -104, 40, 2, -101, -124, 60, -52, -105, -70, -24, -62, 88, 38, 53, -99, -92, 31, 119, 26, 79, 60, -124, 25, -115, -89, -115, -109, 0, 6, 122, -78, 116, 82, 3, 39, -67, 45, -43, 17, -39, 2, 3, 1, 0, 1, -93, 33, 48, 31, 48, 29, 6, 3, 85, 29, 14, 4, 22, 4, 20, 63, 109, -42, -109, 25, 22, 7, -37, -22, -41, -38, 58, -56, 2, -68, -38, -22, 65, -28, -60, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 3, -126, 1, 1, 0, 94, 17, 31, 36, 85, -11, 85, 44, 19, -80, -20, -92, -118, 93, 40, 45, 96, 31, -3, -37, -110, -96, 102, 81, 61, -74, -125, -117, -112, 58, -47, 17, 78, -18, 111, -116, 26, -91, 73, 100, 84, -99, 21, 87, 73, -106, 108, -51, -125, -21, 119, -88, -78, 2, 82, -109, -64, -9, -86, -112, -115, 66, -86, 46, 71, 107, -65, 96, -102, 47, 35, -45, -126, 33, 34, 121, -25, -85, -121, -56, -42, 22, -1, -95, -86, 81, 100, -70, 113, 104, -73, 22, -19, 79, -19, 52, 62, 42, 76, -112, 94, -34, 42, -57, -75, -90, -58, 118, 127, -106, -39, 108, -56, -79, 103, -33, 22, 3, 47, 103, -76, -81, 53, -22, -44, -26, -102, 63, -99, 39, 38, -108, 75, 33, 10, 25, -110, -125, -115, 114, -69, 73, -112, 36, 74, 77, -82, -44, 29, -123, -8, -117, 71, -105, 15, -109, 51, 22, 4, 80, 1, 43, 118, 121, -113, -70, 83, -56, 82, -110, 4, -63, 16, -57, 126, -70, 81, 73, 61, 2, -61, 24, -14, -10, 4, -21, 90, 24, 66, 41, -57, -60, -113, -18, -54, -1, 103, -75, 32, -64, 67, 103, 109, -79, -12, -113, -27, 114, 89, 116, 115, -13, -123, -70, 61, -41, -46, -118, 29, -105, -97, -75, 39, -51, 60, 88, 125, 55, -46, -95, 52, 57, 52, -115, 80, 44, 109, 119, -116, -62, -77, -74, -88, 41, 57, -65, -71, -115, -67, 23, 66, -21, 56, 51, -91, 109},};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.beemdevelopment.aegis.database;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.room.Database;
|
||||
import androidx.room.Room;
|
||||
import androidx.room.RoomDatabase;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@Database(entities = {AuditLogEntry.class}, version = 1)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
public abstract AuditLogDao auditLogDao();
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.beemdevelopment.aegis.database;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.Query;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Dao
|
||||
public interface AuditLogDao {
|
||||
@Insert
|
||||
void insert(AuditLogEntry log);
|
||||
|
||||
@Query("SELECT * FROM audit_logs WHERE timestamp >= strftime('%s', 'now', '-30 days') ORDER BY timestamp DESC")
|
||||
LiveData<List<AuditLogEntry>> getAll();
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.beemdevelopment.aegis.database;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Ignore;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
import com.beemdevelopment.aegis.EventType;
|
||||
|
||||
@Entity(tableName = "audit_logs")
|
||||
public class AuditLogEntry {
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
protected long id;
|
||||
|
||||
@NonNull
|
||||
@ColumnInfo(name = "event_type")
|
||||
private final EventType _eventType;
|
||||
|
||||
@ColumnInfo(name = "reference")
|
||||
private final String _reference;
|
||||
|
||||
@ColumnInfo(name = "timestamp")
|
||||
private final long _timestamp;
|
||||
|
||||
@Ignore
|
||||
public AuditLogEntry(@NonNull EventType eventType) {
|
||||
this(eventType, null);
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public AuditLogEntry(@NonNull EventType eventType, @Nullable String reference) {
|
||||
_eventType = eventType;
|
||||
_reference = reference;
|
||||
_timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
AuditLogEntry(long id, @NonNull EventType eventType, @Nullable String reference, long timestamp) {
|
||||
this.id = id;
|
||||
_eventType = eventType;
|
||||
_reference = reference;
|
||||
_timestamp = timestamp;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public EventType getEventType() {
|
||||
return _eventType;
|
||||
}
|
||||
|
||||
public String getReference() {
|
||||
return _reference;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return _timestamp;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.beemdevelopment.aegis.database;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import com.beemdevelopment.aegis.EventType;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class AuditLogRepository {
|
||||
private final AuditLogDao _auditLogDao;
|
||||
private final Executor _executor;
|
||||
|
||||
public AuditLogRepository(AuditLogDao auditLogDao) {
|
||||
_auditLogDao = auditLogDao;
|
||||
_executor = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
|
||||
public LiveData<List<AuditLogEntry>> getAllAuditLogEntries() {
|
||||
return _auditLogDao.getAll();
|
||||
}
|
||||
|
||||
public void addVaultUnlockedEvent() {
|
||||
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_UNLOCKED);
|
||||
insert(auditLogEntry);
|
||||
}
|
||||
|
||||
public void addBackupCreatedEvent() {
|
||||
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_BACKUP_CREATED);
|
||||
insert(auditLogEntry);
|
||||
}
|
||||
|
||||
public void addAndroidBackupCreatedEvent() {
|
||||
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_ANDROID_BACKUP_CREATED);
|
||||
insert(auditLogEntry);
|
||||
}
|
||||
|
||||
public void addVaultExportedEvent() {
|
||||
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_EXPORTED);
|
||||
insert(auditLogEntry);
|
||||
}
|
||||
|
||||
public void addEntrySharedEvent(String reference) {
|
||||
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.ENTRY_SHARED, reference);
|
||||
insert(auditLogEntry);
|
||||
}
|
||||
|
||||
public void addVaultUnlockFailedPasswordEvent() {
|
||||
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_UNLOCK_FAILED_PASSWORD);
|
||||
insert(auditLogEntry);
|
||||
|
||||
}
|
||||
|
||||
public void addVaultUnlockFailedBiometricsEvent() {
|
||||
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_UNLOCK_FAILED_BIOMETRICS);
|
||||
insert(auditLogEntry);
|
||||
}
|
||||
|
||||
public void insert(AuditLogEntry auditLogEntry) {
|
||||
_executor.execute(() -> {
|
||||
_auditLogDao.insert(auditLogEntry);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
import com.google.common.io.BaseEncoding;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
|
||||
public class Base32 {
|
||||
private Base32() {
|
||||
|
||||
}
|
||||
|
||||
public static byte[] decode(String s) throws EncodingException {
|
||||
try {
|
||||
return BaseEncoding.base32().decode(s.toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new EncodingException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String encode(byte[] data) {
|
||||
return BaseEncoding.base32().omitPadding().encode(data);
|
||||
}
|
||||
|
||||
public static String encode(String s) {
|
||||
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
|
||||
return encode(bytes);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
import com.google.common.io.BaseEncoding;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class Base64 {
|
||||
private Base64() {
|
||||
|
||||
}
|
||||
|
||||
public static byte[] decode(String s) throws EncodingException {
|
||||
try {
|
||||
return BaseEncoding.base64().decode(s);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new EncodingException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] decode(byte[] s) throws EncodingException {
|
||||
return decode(new String(s, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public static String encode(byte[] data) {
|
||||
return BaseEncoding.base64().encode(data);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class EncodingException extends IOException {
|
||||
public EncodingException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public EncodingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.beemdevelopment.aegis.encoding;
|
||||
|
||||
import com.google.common.io.BaseEncoding;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class Hex {
|
||||
private Hex() {
|
||||
|
||||
}
|
||||
|
||||
public static byte[] decode(String s) throws EncodingException {
|
||||
try {
|
||||
return BaseEncoding.base16().decode(s.toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new EncodingException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String encode(byte[] data) {
|
||||
return BaseEncoding.base16().lowerCase().encode(data);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.provider.Settings;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.view.animation.LayoutAnimationController;
|
||||
|
||||
public class AnimationsHelper {
|
||||
private AnimationsHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static Animation loadScaledAnimation(Context context, int animationResId) {
|
||||
return loadScaledAnimation(context, animationResId, Scale.ANIMATOR);
|
||||
}
|
||||
|
||||
public static Animation loadScaledAnimation(Context context, int animationResId, Scale scale) {
|
||||
Animation animation = AnimationUtils.loadAnimation(context, animationResId);
|
||||
long newDuration = (long) (animation.getDuration() * scale.getValue(context));
|
||||
animation.setDuration(newDuration);
|
||||
return animation;
|
||||
}
|
||||
|
||||
public static LayoutAnimationController loadScaledLayoutAnimation(Context context, int animationResId) {
|
||||
return loadScaledLayoutAnimation(context, animationResId, Scale.ANIMATOR);
|
||||
}
|
||||
|
||||
public static LayoutAnimationController loadScaledLayoutAnimation(Context context, int animationResId, Scale scale) {
|
||||
LayoutAnimationController controller = AnimationUtils.loadLayoutAnimation(context, animationResId);
|
||||
Animation animation = controller.getAnimation();
|
||||
animation.setDuration((long) (animation.getDuration() * scale.getValue(context)));
|
||||
return controller;
|
||||
}
|
||||
|
||||
public enum Scale {
|
||||
ANIMATOR(Settings.Global.ANIMATOR_DURATION_SCALE),
|
||||
TRANSITION(Settings.Global.TRANSITION_ANIMATION_SCALE);
|
||||
|
||||
private final String _setting;
|
||||
|
||||
Scale(String setting) {
|
||||
_setting = setting;
|
||||
}
|
||||
|
||||
public float getValue(Context context) {
|
||||
return Settings.Global.getFloat(context.getContentResolver(), _setting, 1.0f);
|
||||
}
|
||||
|
||||
public boolean isZero(Context context) {
|
||||
return getValue(context) == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
/**
|
||||
* A class that can prepare initialization of a BiometricSlot by generating a new
|
||||
* key in the Android KeyStore and authenticating a cipher for it through a
|
||||
* BiometricPrompt.
|
||||
*/
|
||||
public class BiometricSlotInitializer extends BiometricPrompt.AuthenticationCallback {
|
||||
private BiometricSlot _slot;
|
||||
private Listener _listener;
|
||||
private BiometricPrompt _prompt;
|
||||
|
||||
public BiometricSlotInitializer(Fragment fragment, Listener listener) {
|
||||
_listener = listener;
|
||||
_prompt = new BiometricPrompt(fragment, new UiThreadExecutor(), this);
|
||||
}
|
||||
|
||||
public BiometricSlotInitializer(FragmentActivity activity, Listener listener) {
|
||||
_listener = listener;
|
||||
_prompt = new BiometricPrompt(activity, new UiThreadExecutor(), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new key in the Android KeyStore for the new BiometricSlot,
|
||||
* initializes a cipher with it and shows a BiometricPrompt to the user for
|
||||
* authentication. If authentication is successful, the new slot will be
|
||||
* initialized and delivered back through the listener.
|
||||
*/
|
||||
public void authenticate(BiometricPrompt.PromptInfo info) {
|
||||
if (_slot != null) {
|
||||
throw new IllegalStateException("Biometric authentication already in progress");
|
||||
}
|
||||
|
||||
KeyStoreHandle keyStore;
|
||||
try {
|
||||
keyStore = new KeyStoreHandle();
|
||||
} catch (KeyStoreHandleException e) {
|
||||
fail(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// generate a new Android KeyStore key
|
||||
// and assign it the UUID of the new slot as an alias
|
||||
Cipher cipher;
|
||||
BiometricSlot slot = new BiometricSlot();
|
||||
try {
|
||||
SecretKey key = keyStore.generateKey(slot.getUUID().toString());
|
||||
cipher = Slot.createEncryptCipher(key);
|
||||
} catch (KeyStoreHandleException | SlotException e) {
|
||||
fail(e);
|
||||
return;
|
||||
}
|
||||
|
||||
_slot = slot;
|
||||
_prompt.authenticate(info, new BiometricPrompt.CryptoObject(cipher));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the BiometricPrompt and resets the state of the initializer. It will
|
||||
* also attempt to delete the previously generated Android KeyStore key.
|
||||
*/
|
||||
public void cancelAuthentication() {
|
||||
if (_slot == null) {
|
||||
throw new IllegalStateException("Biometric authentication not in progress");
|
||||
}
|
||||
|
||||
reset();
|
||||
_prompt.cancelAuthentication();
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
if (_slot != null) {
|
||||
try {
|
||||
// clean up the unused KeyStore key
|
||||
// this is non-critical, so just fail silently if an error occurs
|
||||
String uuid = _slot.getUUID().toString();
|
||||
KeyStoreHandle keyStore = new KeyStoreHandle();
|
||||
if (keyStore.containsKey(uuid)) {
|
||||
keyStore.deleteKey(uuid);
|
||||
}
|
||||
} catch (KeyStoreHandleException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
_slot = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void fail(int errorCode, CharSequence errString) {
|
||||
reset();
|
||||
_listener.onSlotInitializationFailed(errorCode, errString);
|
||||
}
|
||||
|
||||
private void fail(Exception e) {
|
||||
e.printStackTrace();
|
||||
fail(0, e.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
|
||||
super.onAuthenticationError(errorCode, errString);
|
||||
fail(errorCode, errString.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
|
||||
super.onAuthenticationSucceeded(result);
|
||||
_listener.onInitializeSlot(_slot, Objects.requireNonNull(result.getCryptoObject()).getCipher());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed();
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onInitializeSlot(BiometricSlot slot, Cipher cipher);
|
||||
void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.biometric.BiometricManager;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
|
||||
public class BiometricsHelper {
|
||||
private BiometricsHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static BiometricManager getManager(Context context) {
|
||||
BiometricManager manager = BiometricManager.from(context);
|
||||
if (manager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS) {
|
||||
return manager;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isCanceled(int errorCode) {
|
||||
return errorCode == BiometricPrompt.ERROR_CANCELED
|
||||
|| errorCode == BiometricPrompt.ERROR_USER_CANCELED
|
||||
|| errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON;
|
||||
}
|
||||
|
||||
public static boolean isAvailable(Context context) {
|
||||
return getManager(context) != null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Objects;
|
||||
|
||||
public class BitmapHelper {
|
||||
private BitmapHelper() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Scales the given Bitmap to the given maximum width/height, while keeping the aspect ratio intact.
|
||||
*/
|
||||
public static Bitmap resize(Bitmap bitmap, int maxWidth, int maxHeight) {
|
||||
if (maxHeight <= 0 || maxWidth <= 0) {
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
float maxRatio = (float) maxWidth / maxHeight;
|
||||
float ratio = (float) bitmap.getWidth() / bitmap.getHeight();
|
||||
|
||||
int width = maxWidth;
|
||||
int height = maxHeight;
|
||||
if (maxRatio > 1) {
|
||||
width = (int) ((float) maxHeight * ratio);
|
||||
} else {
|
||||
height = (int) ((float) maxWidth / ratio);
|
||||
}
|
||||
|
||||
return Bitmap.createScaledBitmap(bitmap, width, height, true);
|
||||
}
|
||||
|
||||
public static boolean isVaultEntryIconOptimized(VaultEntryIcon icon) {
|
||||
BitmapFactory.Options opts = new BitmapFactory.Options();
|
||||
opts.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeByteArray(icon.getBytes(), 0, icon.getBytes().length, opts);
|
||||
return opts.outWidth <= VaultEntryIcon.MAX_DIMENS && opts.outHeight <= VaultEntryIcon.MAX_DIMENS;
|
||||
}
|
||||
|
||||
public static VaultEntryIcon toVaultEntryIcon(Bitmap bitmap, IconType iconType) {
|
||||
if (bitmap.getWidth() > VaultEntryIcon.MAX_DIMENS
|
||||
|| bitmap.getHeight() > VaultEntryIcon.MAX_DIMENS) {
|
||||
bitmap = resize(bitmap, VaultEntryIcon.MAX_DIMENS, VaultEntryIcon.MAX_DIMENS);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
if (Objects.equals(iconType, IconType.PNG)) {
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
|
||||
} else {
|
||||
iconType = IconType.JPEG;
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
|
||||
}
|
||||
|
||||
byte[] data = stream.toByteArray();
|
||||
return new VaultEntryIcon(data, iconType);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.MetricAffectingSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class CenterVerticalSpan extends MetricAffectingSpan {
|
||||
Rect _substringBounds;
|
||||
|
||||
public CenterVerticalSpan(Rect substringBounds) {
|
||||
_substringBounds = substringBounds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMeasureState(@NonNull TextPaint textPaint) {
|
||||
applyBaselineShift(textPaint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(@NonNull TextPaint textPaint) {
|
||||
applyBaselineShift(textPaint);
|
||||
}
|
||||
|
||||
private void applyBaselineShift(TextPaint textPaint) {
|
||||
float topDifference = textPaint.getFontMetrics().top - _substringBounds.top;
|
||||
textPaint.baselineShift -= (topDifference / 2f);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
|
||||
import androidx.activity.ComponentActivity;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* ContextHelper contains some disgusting hacks to obtain the Activity/Lifecycle from a Context.
|
||||
*/
|
||||
public class ContextHelper {
|
||||
private ContextHelper() {
|
||||
|
||||
}
|
||||
|
||||
// source: https://github.com/androidx/androidx/blob/e32e1da51a0c7448c74861c667fa76738a415a89/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteButton.java#L425-L435
|
||||
@Nullable
|
||||
public static ComponentActivity getActivity(@NonNull Context context) {
|
||||
while (context instanceof ContextWrapper) {
|
||||
if (context instanceof ComponentActivity) {
|
||||
return (ComponentActivity) context;
|
||||
}
|
||||
|
||||
context = ((ContextWrapper) context).getBaseContext();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Lifecycle getLifecycle(@NonNull Context context) {
|
||||
ComponentActivity activity = getActivity(context);
|
||||
return activity == null ? null : activity.getLifecycle();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
|
||||
import androidx.annotation.ArrayRes;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DropdownHelper {
|
||||
private DropdownHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static void fillDropdown(Context context, AutoCompleteTextView dropdown, @ArrayRes int textArrayResId) {
|
||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(context, textArrayResId, R.layout.dropdown_list_item);
|
||||
dropdown.setAdapter(adapter);
|
||||
}
|
||||
|
||||
public static <T> void fillDropdown(Context context, AutoCompleteTextView dropdown, List<T> items) {
|
||||
ArrayAdapter<T> adapter = new ArrayAdapter<>(context, R.layout.dropdown_list_item, items);
|
||||
dropdown.setAdapter(adapter);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.widget.EditText;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class EditTextHelper {
|
||||
private EditTextHelper() {
|
||||
}
|
||||
|
||||
public static char[] getEditTextChars(EditText text) {
|
||||
Editable editable = text.getText();
|
||||
char[] chars = new char[editable.length()];
|
||||
editable.getChars(0, editable.length(), chars, 0);
|
||||
return chars;
|
||||
}
|
||||
|
||||
public static boolean areEditTextsEqual(EditText text1, EditText text2) {
|
||||
char[] password = getEditTextChars(text1);
|
||||
char[] passwordConfirm = getEditTextChars(text2);
|
||||
return password.length != 0 && Arrays.equals(password, passwordConfirm);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.view.View;
|
||||
import android.view.animation.AccelerateInterpolator;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
|
||||
public class FabScrollHelper {
|
||||
private View _fabMenu;
|
||||
private boolean _isAnimating;
|
||||
|
||||
public FabScrollHelper(View floatingActionsMenu) {
|
||||
_fabMenu = floatingActionsMenu;
|
||||
}
|
||||
|
||||
public void onScroll(int dx, int dy) {
|
||||
if (dy > 2 && _fabMenu.getVisibility() == View.VISIBLE && !_isAnimating) {
|
||||
setVisible(false);
|
||||
} else if (dy < -2 && _fabMenu.getVisibility() != View.VISIBLE && !_isAnimating) {
|
||||
setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void setVisible(boolean visible) {
|
||||
if (visible) {
|
||||
_fabMenu.setVisibility(View.VISIBLE);
|
||||
_fabMenu.animate()
|
||||
.translationY(0)
|
||||
.setInterpolator(new DecelerateInterpolator(2))
|
||||
.setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
_isAnimating = false;
|
||||
super.onAnimationEnd(animation);
|
||||
}
|
||||
}).start();
|
||||
} else {
|
||||
_isAnimating = true;
|
||||
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) _fabMenu.getLayoutParams();
|
||||
int fabBottomMargin = lp.bottomMargin;
|
||||
_fabMenu.animate()
|
||||
.translationY(_fabMenu.getHeight() + fabBottomMargin)
|
||||
.setInterpolator(new AccelerateInterpolator(2))
|
||||
.setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
_isAnimating = false;
|
||||
_fabMenu.setVisibility(View.INVISIBLE);
|
||||
super.onAnimationEnd(animation);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public interface ItemTouchHelperAdapter {
|
||||
|
||||
/**
|
||||
* Called when an item has been dragged far enough to trigger a move. This is called every time
|
||||
* an item is shifted, and <strong>not</strong> at the end of a "drop" event.<br/>
|
||||
* <br/>
|
||||
* Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after
|
||||
* adjusting the underlying data to reflect this move.
|
||||
*
|
||||
* @param fromPosition The start position of the moved item.
|
||||
* @param toPosition Then resolved position of the moved item.
|
||||
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
|
||||
* @see RecyclerView.ViewHolder#getAdapterPosition()
|
||||
*/
|
||||
void onItemMove(int fromPosition, int toPosition);
|
||||
|
||||
/**
|
||||
* Called when an item has been dismissed by a swipe.<br/>
|
||||
* <br/>
|
||||
* Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after
|
||||
* adjusting the underlying data to reflect this removal.
|
||||
*
|
||||
* @param position The position of the item dismissed.
|
||||
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
|
||||
* @see RecyclerView.ViewHolder#getAdapterPosition()
|
||||
*/
|
||||
void onItemDismiss(int position);
|
||||
|
||||
/**
|
||||
* Called when an item has been dropped after a drag.
|
||||
*
|
||||
* @param position The position of the moved item.
|
||||
*/
|
||||
void onItemDrop(int position);
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
public class MetricsHelper {
|
||||
private MetricsHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static int convertDpToPixels(Context context, float dp) {
|
||||
return (int) (dp * (context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.google.common.base.Strings;
|
||||
import com.nulabinc.zxcvbn.Strength;
|
||||
import com.nulabinc.zxcvbn.Zxcvbn;
|
||||
|
||||
public class PasswordStrengthHelper {
|
||||
// Limit the password length to prevent zxcvbn4j from exploding
|
||||
private static final int MAX_PASSWORD_LENGTH = 64;
|
||||
|
||||
// Material design color palette
|
||||
private final static String[] COLORS = {"#FF5252", "#FF5252", "#FFC107", "#8BC34A", "#4CAF50"};
|
||||
|
||||
private final Zxcvbn _zxcvbn = new Zxcvbn();
|
||||
private final EditText _textPassword;
|
||||
private final ProgressBar _barPasswordStrength;
|
||||
private final TextView _textPasswordStrength;
|
||||
private final TextInputLayout _textPasswordWrapper;
|
||||
|
||||
public PasswordStrengthHelper(
|
||||
EditText textPassword,
|
||||
ProgressBar barPasswordStrength,
|
||||
TextView textPasswordStrength,
|
||||
TextInputLayout textPasswordWrapper
|
||||
) {
|
||||
_textPassword = textPassword;
|
||||
_barPasswordStrength = barPasswordStrength;
|
||||
_textPasswordStrength = textPasswordStrength;
|
||||
_textPasswordWrapper = textPasswordWrapper;
|
||||
}
|
||||
|
||||
public void measure(Context context) {
|
||||
if (_textPassword.getText().length() > MAX_PASSWORD_LENGTH) {
|
||||
_barPasswordStrength.setProgress(0);
|
||||
_textPasswordStrength.setText(R.string.password_strength_unknown);
|
||||
} else {
|
||||
Strength strength = _zxcvbn.measure(_textPassword.getText());
|
||||
_barPasswordStrength.setProgress(strength.getScore());
|
||||
_barPasswordStrength.setProgressTintList(ColorStateList.valueOf(Color.parseColor(getColor(strength.getScore()))));
|
||||
_textPasswordStrength.setText((_textPassword.getText().length() != 0) ? getString(strength.getScore(), context) : "");
|
||||
String warning = strength.getFeedback().getWarning();
|
||||
_textPasswordWrapper.setError(warning);
|
||||
_textPasswordWrapper.setErrorEnabled(!Strings.isNullOrEmpty(warning));
|
||||
strength.wipe();
|
||||
}
|
||||
}
|
||||
|
||||
private static String getString(int score, Context context) {
|
||||
if (score < 0 || score > 4) {
|
||||
throw new IllegalArgumentException("Not a valid zxcvbn score");
|
||||
}
|
||||
|
||||
String[] strings = context.getResources().getStringArray(R.array.password_strength);
|
||||
return strings[score];
|
||||
}
|
||||
|
||||
private static String getColor(int score) {
|
||||
if (score < 0 || score > 4) {
|
||||
throw new IllegalArgumentException("Not a valid zxcvbn score");
|
||||
}
|
||||
|
||||
return COLORS[score];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
public class PermissionHelper {
|
||||
private PermissionHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static boolean granted(Context context, String permission) {
|
||||
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
public static boolean request(Activity activity, int requestCode, String... perms) {
|
||||
List<String> deniedPerms = new ArrayList<>();
|
||||
for (String permission : perms) {
|
||||
if (!granted(activity, permission)) {
|
||||
deniedPerms.add(permission);
|
||||
}
|
||||
}
|
||||
|
||||
int size = deniedPerms.size();
|
||||
if (size > 0) {
|
||||
String[] array = new String[size];
|
||||
ActivityCompat.requestPermissions(activity, deniedPerms.toArray(array), requestCode);
|
||||
}
|
||||
return size == 0;
|
||||
}
|
||||
|
||||
public static boolean checkResults(int[] grantResults) {
|
||||
for (int result : grantResults) {
|
||||
if (result != PackageManager.PERMISSION_GRANTED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import static android.graphics.ImageFormat.YUV_420_888;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.Size;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.camera.core.ImageAnalysis;
|
||||
import androidx.camera.core.ImageProxy;
|
||||
|
||||
import com.google.zxing.NotFoundException;
|
||||
import com.google.zxing.PlanarYUVLuminanceSource;
|
||||
import com.google.zxing.Result;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class QrCodeAnalyzer implements ImageAnalysis.Analyzer {
|
||||
private static final String TAG = QrCodeAnalyzer.class.getSimpleName();
|
||||
public static final Size RESOLUTION = new Size(1200, 1600);
|
||||
|
||||
private final QrCodeAnalyzer.Listener _listener;
|
||||
|
||||
public QrCodeAnalyzer(QrCodeAnalyzer.Listener listener) {
|
||||
_listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void analyze(@NonNull ImageProxy image) {
|
||||
int format = image.getFormat();
|
||||
if (format != YUV_420_888) {
|
||||
Log.e(TAG, String.format("Unexpected YUV image format: %d", format));
|
||||
image.close();
|
||||
return;
|
||||
}
|
||||
|
||||
ImageProxy.PlaneProxy plane = image.getPlanes()[0];
|
||||
ByteBuffer buf = plane.getBuffer();
|
||||
byte[] data = new byte[buf.remaining()];
|
||||
buf.get(data);
|
||||
buf.rewind();
|
||||
|
||||
PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(
|
||||
data,
|
||||
plane.getRowStride(),
|
||||
image.getHeight(),
|
||||
0,
|
||||
0,
|
||||
image.getWidth(),
|
||||
image.getHeight(),
|
||||
false
|
||||
);
|
||||
|
||||
try {
|
||||
Result result = QrCodeHelper.decodeFromSource(source);
|
||||
if (_listener != null) {
|
||||
_listener.onQrCodeDetected(result);
|
||||
}
|
||||
} catch (NotFoundException ignored) {
|
||||
|
||||
} finally {
|
||||
image.close();
|
||||
}
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onQrCodeDetected(Result result);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.BinaryBitmap;
|
||||
import com.google.zxing.DecodeHintType;
|
||||
import com.google.zxing.LuminanceSource;
|
||||
import com.google.zxing.MultiFormatReader;
|
||||
import com.google.zxing.NotFoundException;
|
||||
import com.google.zxing.RGBLuminanceSource;
|
||||
import com.google.zxing.Result;
|
||||
import com.google.zxing.WriterException;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
import com.google.zxing.common.HybridBinarizer;
|
||||
import com.google.zxing.qrcode.QRCodeWriter;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class QrCodeHelper {
|
||||
private QrCodeHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static Result decodeFromSource(LuminanceSource source) throws NotFoundException {
|
||||
Map<DecodeHintType, Object> hints = new HashMap<>();
|
||||
hints.put(DecodeHintType.POSSIBLE_FORMATS, Collections.singletonList(BarcodeFormat.QR_CODE));
|
||||
hints.put(DecodeHintType.ALSO_INVERTED, true);
|
||||
|
||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
MultiFormatReader reader = new MultiFormatReader();
|
||||
return reader.decode(bitmap, hints);
|
||||
}
|
||||
|
||||
public static Result decodeFromStream(InputStream inStream) throws DecodeError {
|
||||
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
|
||||
Bitmap bitmap = BitmapFactory.decodeStream(inStream, null, bmOptions);
|
||||
if (bitmap == null) {
|
||||
throw new DecodeError("Unable to decode stream to bitmap");
|
||||
}
|
||||
|
||||
// If ZXing is not able to decode the image on the first try, we try a couple of
|
||||
// more times with smaller versions of the same image.
|
||||
for (int i = 0; i <= 2; i++) {
|
||||
if (i != 0) {
|
||||
bitmap = BitmapHelper.resize(bitmap, bitmap.getWidth() / (i * 2), bitmap.getHeight() / (i * 2));
|
||||
}
|
||||
|
||||
try {
|
||||
int[] pixels = new int[bitmap.getWidth() * bitmap.getHeight()];
|
||||
bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||
|
||||
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), pixels);
|
||||
return decodeFromSource(source);
|
||||
} catch (NotFoundException ignored) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
throw new DecodeError(NotFoundException.getNotFoundInstance());
|
||||
}
|
||||
|
||||
public static Bitmap encodeToBitmap(String data, int width, int height, @ColorInt int backgroundColor) throws WriterException {
|
||||
QRCodeWriter writer = new QRCodeWriter();
|
||||
BitMatrix bitMatrix = writer.encode(data, BarcodeFormat.QR_CODE, width, height);
|
||||
|
||||
int[] pixels = new int[width * height];
|
||||
for (int y = 0; y < height; y++) {
|
||||
int offset = y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
pixels[offset + x] = bitMatrix.get(x, y) ? Color.BLACK : backgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public static class DecodeError extends Exception {
|
||||
public DecodeError(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public DecodeError(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
public class SafHelper {
|
||||
private SafHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static String getFileName(Context context, Uri uri) {
|
||||
if (uri.getScheme() != null && uri.getScheme().equals("content")) {
|
||||
try (Cursor cursor = context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int i = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
if (i != -1) {
|
||||
return cursor.getString(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return uri.getLastPathSegment();
|
||||
}
|
||||
|
||||
public static String getMimeType(Context context, Uri uri) {
|
||||
DocumentFile file = DocumentFile.fromSingleUri(context, uri);
|
||||
if (file != null) {
|
||||
String fileType = file.getType();
|
||||
if (fileType != null) {
|
||||
return fileType;
|
||||
}
|
||||
|
||||
String ext = MimeTypeMap.getFileExtensionFromUrl(uri.toString());
|
||||
if (ext != null) {
|
||||
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.view.animation.Animation;
|
||||
|
||||
public class SimpleAnimationEndListener implements Animation.AnimationListener {
|
||||
private final Listener _listener;
|
||||
|
||||
public SimpleAnimationEndListener(Listener listener) {
|
||||
_listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationStart(Animation animation) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
if (_listener != null) {
|
||||
_listener.onAnimationEnd(animation);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animation animation) {
|
||||
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onAnimationEnd(Animation animation);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.ui.views.EntryAdapter;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
|
||||
private VaultEntry _selectedEntry;
|
||||
|
||||
private final EntryAdapter _adapter;
|
||||
private boolean _positionChanged = false;
|
||||
private boolean _isLongPressDragEnabled = true;
|
||||
private int _dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
|
||||
|
||||
public SimpleItemTouchHelperCallback(EntryAdapter adapter) {
|
||||
_adapter = adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLongPressDragEnabled() {
|
||||
return _isLongPressDragEnabled;
|
||||
}
|
||||
|
||||
public void setIsLongPressDragEnabled(boolean enabled) {
|
||||
_isLongPressDragEnabled = enabled;
|
||||
}
|
||||
|
||||
public void setSelectedEntry(VaultEntry entry) {
|
||||
if (entry == null) {
|
||||
_selectedEntry = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!entry.isFavorite()) {
|
||||
_selectedEntry = entry;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isItemViewSwipeEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setDragFlags(int dragFlags) {
|
||||
_dragFlags = dragFlags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
// It's not clear when this can happen, but sometimes the ViewHolder
|
||||
// that's passed to this function has a position of -1, leading
|
||||
// to a crash down the line.
|
||||
int position = viewHolder.getBindingAdapterPosition();
|
||||
if (position == NO_POSITION) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
EntryAdapter adapter = (EntryAdapter) recyclerView.getAdapter();
|
||||
if (adapter == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int swipeFlags = 0;
|
||||
if (adapter.isPositionFooter(position)
|
||||
|| adapter.isPositionErrorCard(position)
|
||||
|| adapter.getEntryAtPosition(position) != _selectedEntry
|
||||
|| !isLongPressDragEnabled()) {
|
||||
return makeMovementFlags(0, swipeFlags);
|
||||
}
|
||||
|
||||
return makeMovementFlags(_dragFlags, swipeFlags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
|
||||
RecyclerView.ViewHolder target) {
|
||||
int targetIndex = _adapter.translateEntryPosToIndex(target.getBindingAdapterPosition());
|
||||
if (targetIndex < _adapter.getShownFavoritesCount()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int firstPosition = viewHolder.getLayoutPosition();
|
||||
int secondPosition = target.getBindingAdapterPosition();
|
||||
|
||||
_adapter.onItemMove(firstPosition, secondPosition);
|
||||
_positionChanged = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
_adapter.onItemDismiss(viewHolder.getBindingAdapterPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
|
||||
if (_positionChanged) {
|
||||
_adapter.onItemDrop(viewHolder.getBindingAdapterPosition());
|
||||
_positionChanged = false;
|
||||
_adapter.refresh(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
|
||||
public final class SimpleTextWatcher implements TextWatcher {
|
||||
private final Listener _listener;
|
||||
|
||||
public SimpleTextWatcher(Listener listener) {
|
||||
_listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (_listener != null) {
|
||||
_listener.afterTextChanged(s);
|
||||
}
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void afterTextChanged(Editable s);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
import com.amulyakhare.textdrawable.util.ColorGenerator;
|
||||
|
||||
import java.text.BreakIterator;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class TextDrawableHelper {
|
||||
// taken from: https://materialuicolors.co (level 700)
|
||||
private static ColorGenerator _generator = ColorGenerator.create(Arrays.asList(
|
||||
0xFFD32F2F,
|
||||
0xFFC2185B,
|
||||
0xFF7B1FA2,
|
||||
0xFF512DA8,
|
||||
0xFF303F9F,
|
||||
0xFF1976D2,
|
||||
0xFF0288D1,
|
||||
0xFF0097A7,
|
||||
0xFF00796B,
|
||||
0xFF388E3C,
|
||||
0xFF689F38,
|
||||
0xFFAFB42B,
|
||||
0xFFFBC02D,
|
||||
0xFFFFA000,
|
||||
0xFFF57C00,
|
||||
0xFFE64A19,
|
||||
0xFF5D4037,
|
||||
0xFF616161,
|
||||
0xFF455A64
|
||||
));
|
||||
|
||||
private TextDrawableHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static TextDrawable generate(String text, String fallback, View view) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
if (fallback == null || fallback.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
text = fallback;
|
||||
}
|
||||
|
||||
int color = _generator.getColor(text);
|
||||
return TextDrawable.builder().beginConfig()
|
||||
.width(view.getLayoutParams().width)
|
||||
.height(view.getLayoutParams().height)
|
||||
.endConfig()
|
||||
.buildRound(getFirstGrapheme(text).toUpperCase(), color);
|
||||
}
|
||||
|
||||
private static String getFirstGrapheme(String text) {
|
||||
BreakIterator iter = BreakIterator.getCharacterInstance();
|
||||
iter.setText(text);
|
||||
|
||||
int start = iter.first(), end = iter.next();
|
||||
if (end == BreakIterator.DONE) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return text.substring(start, end);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.google.android.material.color.DynamicColors;
|
||||
import com.google.android.material.color.DynamicColorsOptions;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ThemeHelper {
|
||||
private final AppCompatActivity _activity;
|
||||
private final Preferences _prefs;
|
||||
|
||||
public ThemeHelper(AppCompatActivity activity, Preferences prefs) {
|
||||
_activity = activity;
|
||||
_prefs = prefs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the theme of the activity. The actual style that is set is picked from the
|
||||
* given map, based on the theme configured by the user.
|
||||
*/
|
||||
public void setTheme(Map<Theme, Integer> themeMap) {
|
||||
int theme = themeMap.get(getConfiguredTheme());
|
||||
_activity.setTheme(theme);
|
||||
|
||||
if (_prefs.isDynamicColorsEnabled()) {
|
||||
DynamicColorsOptions.Builder optsBuilder = new DynamicColorsOptions.Builder();
|
||||
if (getConfiguredTheme().equals(Theme.AMOLED)) {
|
||||
optsBuilder.setThemeOverlay(R.style.ThemeOverlay_Aegis_Dynamic_Amoled);
|
||||
} else if (getConfiguredTheme().equals(Theme.DARK)) {
|
||||
optsBuilder.setThemeOverlay(R.style.ThemeOverlay_Aegis_Dynamic_Dark);
|
||||
}
|
||||
|
||||
DynamicColors.applyToActivityIfAvailable(_activity, optsBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
public Theme getConfiguredTheme() {
|
||||
Theme theme = _prefs.getCurrentTheme();
|
||||
|
||||
if (theme == Theme.SYSTEM || theme == Theme.SYSTEM_AMOLED) {
|
||||
int currentNightMode = _activity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
if (currentNightMode == Configuration.UI_MODE_NIGHT_YES) {
|
||||
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
|
||||
} else {
|
||||
theme = Theme.LIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.os.Handler;
|
||||
|
||||
import com.beemdevelopment.aegis.VibrationPatterns;
|
||||
|
||||
public class UiRefresher {
|
||||
private boolean _running;
|
||||
private Listener _listener;
|
||||
private Handler _handler;
|
||||
|
||||
public UiRefresher(Listener listener) {
|
||||
_listener = listener;
|
||||
_handler = new Handler();
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
stop();
|
||||
_listener = null;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (_running) {
|
||||
return;
|
||||
}
|
||||
_running = true;
|
||||
|
||||
_handler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
_listener.onRefresh();
|
||||
_handler.postDelayed(this, _listener.getMillisTillNextRefresh());
|
||||
}
|
||||
}, _listener.getMillisTillNextRefresh());
|
||||
|
||||
_handler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
_listener.onExpiring();
|
||||
_handler.postDelayed(this, getNextRun());
|
||||
}
|
||||
}, getInitialRun());
|
||||
}
|
||||
|
||||
private long getInitialRun() {
|
||||
long sum = _listener.getMillisTillNextRefresh() - VibrationPatterns.getLengthInMillis(VibrationPatterns.EXPIRING);
|
||||
if (sum < 0) {
|
||||
return getNextRun();
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
private long getNextRun() {
|
||||
return (_listener.getMillisTillNextRefresh() + _listener.getPeriodMillis()) - VibrationPatterns.getLengthInMillis(VibrationPatterns.EXPIRING);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
_handler.removeCallbacksAndMessages(null);
|
||||
_running = false;
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onRefresh();
|
||||
void onExpiring();
|
||||
long getMillisTillNextRefresh();
|
||||
long getPeriodMillis();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public class UiThreadExecutor implements Executor {
|
||||
private final Handler _handler = new Handler(Looper.getMainLooper());
|
||||
|
||||
@Override
|
||||
public void execute(@NonNull Runnable command) {
|
||||
_handler.post(command);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.os.VibratorManager;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
|
||||
public class VibrationHelper {
|
||||
private Preferences _preferences;
|
||||
|
||||
public VibrationHelper(Context context) {
|
||||
_preferences = new Preferences(context);
|
||||
}
|
||||
|
||||
public void vibratePattern(Context context, long[] pattern) {
|
||||
if (!isHapticFeedbackEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
VibratorManager vibratorManager = (VibratorManager) context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE);
|
||||
if (vibratorManager != null) {
|
||||
Vibrator vibrator = vibratorManager.getDefaultVibrator();
|
||||
VibrationEffect effect = VibrationEffect.createWaveform(pattern, -1);
|
||||
vibrator.vibrate(effect);
|
||||
}
|
||||
} else {
|
||||
Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
|
||||
if (vibrator != null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
VibrationEffect effect = VibrationEffect.createWaveform(pattern, -1);
|
||||
vibrator.vibrate(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isHapticFeedbackEnabled() {
|
||||
return _preferences.isHapticFeedbackEnabled();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
|
||||
public class ViewHelper {
|
||||
private ViewHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static void setupAppBarInsets(AppBarLayout appBar) {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(appBar, (targetView, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
|
||||
targetView.setPadding(
|
||||
insets.left,
|
||||
insets.top,
|
||||
insets.right,
|
||||
0
|
||||
);
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.beemdevelopment.aegis.helpers.comparators;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
public class AccountNameComparator implements Comparator<VaultEntry> {
|
||||
@Override
|
||||
public int compare(VaultEntry a, VaultEntry b) {
|
||||
return a.getName().compareToIgnoreCase(b.getName());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.beemdevelopment.aegis.helpers.comparators;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
public class FavoriteComparator implements Comparator<VaultEntry> {
|
||||
@Override
|
||||
public int compare(VaultEntry a, VaultEntry b) {
|
||||
return -1 * Boolean.compare(a.isFavorite(), b.isFavorite());
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue