From 171da34b13e95520eb57e41c2545c42d46728edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Sch=C3=A4ttgen?= Date: Sun, 31 Mar 2024 22:32:56 +0200 Subject: [PATCH] Start working on audit logs --- app/build.gradle | 4 + .../aegis/AegisApplicationBase.java | 9 ++ .../aegis/AegisBackupAgent.java | 7 + .../beemdevelopment/aegis/AegisModule.java | 24 ++- .../com/beemdevelopment/aegis/EventType.java | 42 ++++++ .../aegis/database/AppDatabase.java | 15 ++ .../aegis/database/AuditLogDao.java | 17 +++ .../aegis/database/AuditLogEntry.java | 61 ++++++++ .../aegis/database/AuditLogRepository.java | 66 +++++++++ .../aegis/ui/AegisActivity.java | 4 + .../aegis/ui/AuthActivity.java | 3 + .../aegis/ui/MainActivity.java | 4 + .../AuditLogPreferencesFragment.java | 97 ++++++++++++ .../ImportExportPreferencesFragment.java | 7 +- .../preferences/PreferencesFragment.java | 4 + .../aegis/ui/models/AuditLogEntryModel.java | 24 +++ .../aegis/ui/views/AuditLogAdapter.java | 62 ++++++++ .../aegis/ui/views/AuditLogHolder.java | 140 ++++++++++++++++++ .../aegis/vault/VaultBackupManager.java | 6 +- .../aegis/vault/VaultManager.java | 8 +- app/src/main/res/drawable/ic_export_notes.xml | 5 + app/src/main/res/drawable/ic_folder_zip.xml | 5 + app/src/main/res/drawable/ic_lock.xml | 5 + app/src/main/res/drawable/ic_lock_open.xml | 5 + app/src/main/res/drawable/ic_share.xml | 5 + app/src/main/res/drawable/ic_timeline_24.xml | 10 ++ app/src/main/res/layout/card_audit_log.xml | 111 ++++++++++++++ .../main/res/layout/fragment_audit_log.xml | 56 +++++++ app/src/main/res/values/strings.xml | 24 +++ app/src/main/res/values/themes.xml | 5 + app/src/main/res/xml/preferences.xml | 6 + 31 files changed, 834 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/beemdevelopment/aegis/EventType.java create mode 100644 app/src/main/java/com/beemdevelopment/aegis/database/AppDatabase.java create mode 100644 app/src/main/java/com/beemdevelopment/aegis/database/AuditLogDao.java create mode 100644 app/src/main/java/com/beemdevelopment/aegis/database/AuditLogEntry.java create mode 100644 app/src/main/java/com/beemdevelopment/aegis/database/AuditLogRepository.java create mode 100644 app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/AuditLogPreferencesFragment.java create mode 100644 app/src/main/java/com/beemdevelopment/aegis/ui/models/AuditLogEntryModel.java create mode 100644 app/src/main/java/com/beemdevelopment/aegis/ui/views/AuditLogAdapter.java create mode 100644 app/src/main/java/com/beemdevelopment/aegis/ui/views/AuditLogHolder.java create mode 100644 app/src/main/res/drawable/ic_export_notes.xml create mode 100644 app/src/main/res/drawable/ic_folder_zip.xml create mode 100644 app/src/main/res/drawable/ic_lock.xml create mode 100644 app/src/main/res/drawable/ic_lock_open.xml create mode 100644 app/src/main/res/drawable/ic_share.xml create mode 100644 app/src/main/res/drawable/ic_timeline_24.xml create mode 100644 app/src/main/res/layout/card_audit_log.xml create mode 100644 app/src/main/res/layout/fragment_audit_log.xml diff --git a/app/build.gradle b/app/build.gradle index ee8b943e..b7a627bf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -139,10 +139,13 @@ dependencies { def hiltVersion = '2.50' def junitVersion = '4.13.2' def libsuVersion = '5.2.2' + def roomVersion = "2.6.1" + annotationProcessor 'androidx.annotation:annotation:1.7.1' annotationProcessor "com.google.dagger:hilt-compiler:$hiltVersion" annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}" + annotationProcessor "androidx.room:room-compiler:$roomVersion" implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.activity:activity:1.8.2' @@ -157,6 +160,7 @@ dependencies { implementation "androidx.lifecycle:lifecycle-process:2.6.2" implementation "androidx.preference:preference:1.2.1" implementation 'androidx.recyclerview:recyclerview:1.3.2' + implementation "androidx.room:room-runtime:$roomVersion" implementation "androidx.viewpager2:viewpager2:1.0.0" implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation 'com.caverock:androidsvg-aar:1.4' diff --git a/app/src/main/java/com/beemdevelopment/aegis/AegisApplicationBase.java b/app/src/main/java/com/beemdevelopment/aegis/AegisApplicationBase.java index 2d973fa7..998a4ca1 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/AegisApplicationBase.java +++ b/app/src/main/java/com/beemdevelopment/aegis/AegisApplicationBase.java @@ -17,14 +17,23 @@ import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleEventObserver; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.ProcessLifecycleOwner; +import androidx.room.Room; +import com.beemdevelopment.aegis.database.AppDatabase; +import com.beemdevelopment.aegis.database.AuditLogEntry; +import com.beemdevelopment.aegis.database.AuditLogRepository; 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 org.checkerframework.checker.units.qual.A; + import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import dagger.hilt.InstallIn; import dagger.hilt.android.EarlyEntryPoint; diff --git a/app/src/main/java/com/beemdevelopment/aegis/AegisBackupAgent.java b/app/src/main/java/com/beemdevelopment/aegis/AegisBackupAgent.java index 05cf4047..3a08e6e5 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/AegisBackupAgent.java +++ b/app/src/main/java/com/beemdevelopment/aegis/AegisBackupAgent.java @@ -8,6 +8,8 @@ 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; @@ -25,12 +27,16 @@ public class AegisBackupAgent extends BackupAgent { 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 @@ -53,6 +59,7 @@ public class AegisBackupAgent extends BackupAgent { // 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)); diff --git a/app/src/main/java/com/beemdevelopment/aegis/AegisModule.java b/app/src/main/java/com/beemdevelopment/aegis/AegisModule.java index 6172c122..bdaae0d4 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/AegisModule.java +++ b/app/src/main/java/com/beemdevelopment/aegis/AegisModule.java @@ -2,6 +2,11 @@ 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; @@ -24,12 +29,27 @@ public class AegisModule { @Provides @Singleton - public static VaultManager provideVaultManager(@ApplicationContext Context context) { - return new VaultManager(context); + 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(); + } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/EventType.java b/app/src/main/java/com/beemdevelopment/aegis/EventType.java new file mode 100644 index 00000000..4588ee01 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/EventType.java @@ -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; + } + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/database/AppDatabase.java b/app/src/main/java/com/beemdevelopment/aegis/database/AppDatabase.java new file mode 100644 index 00000000..073bb2eb --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/database/AppDatabase.java @@ -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(); +} \ No newline at end of file diff --git a/app/src/main/java/com/beemdevelopment/aegis/database/AuditLogDao.java b/app/src/main/java/com/beemdevelopment/aegis/database/AuditLogDao.java new file mode 100644 index 00000000..20c4a47a --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/database/AuditLogDao.java @@ -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> getAll(); +} \ No newline at end of file diff --git a/app/src/main/java/com/beemdevelopment/aegis/database/AuditLogEntry.java b/app/src/main/java/com/beemdevelopment/aegis/database/AuditLogEntry.java new file mode 100644 index 00000000..b0888eeb --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/database/AuditLogEntry.java @@ -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; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/beemdevelopment/aegis/database/AuditLogRepository.java b/app/src/main/java/com/beemdevelopment/aegis/database/AuditLogRepository.java new file mode 100644 index 00000000..cde042ff --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/database/AuditLogRepository.java @@ -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> 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); + }); + } + +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/AegisActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/AegisActivity.java index 63a59334..a44eaa5e 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/AegisActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/AegisActivity.java @@ -21,6 +21,7 @@ import androidx.core.view.ViewPropertyAnimatorCompat; import com.beemdevelopment.aegis.Preferences; import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.ThemeMap; +import com.beemdevelopment.aegis.database.AuditLogRepository; import com.beemdevelopment.aegis.helpers.ThemeHelper; import com.beemdevelopment.aegis.icons.IconPackManager; import com.beemdevelopment.aegis.vault.VaultManager; @@ -48,6 +49,9 @@ public abstract class AegisActivity extends AppCompatActivity implements VaultMa @Inject protected VaultManager _vaultManager; + @Inject + protected AuditLogRepository _auditLogRepository; + @Inject protected IconPackManager _iconPackManager; diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/AuthActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/AuthActivity.java index 3bf5007e..d711208c 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/AuthActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/AuthActivity.java @@ -344,6 +344,8 @@ public class AuthActivity extends AegisActivity { finish(result.getKey(), result.isSlotRepaired()); } else { _decryptButton.setEnabled(true); + + _auditLogRepository.addVaultUnlockFailedPasswordEvent(); onInvalidPassword(); } } @@ -356,6 +358,7 @@ public class AuthActivity extends AegisActivity { _bioPrompt = null; if (!BiometricsHelper.isCanceled(errorCode)) { + _auditLogRepository.addVaultUnlockFailedBiometricsEvent(); Toast.makeText(AuthActivity.this, errString, Toast.LENGTH_LONG).show(); } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java index 1f9f0505..2fd33f3e 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java @@ -522,6 +522,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene } private void onDecryptResult() { + _auditLogRepository.addVaultUnlockedEvent(); + loadEntries(); checkTimeSyncSetting(); } @@ -1176,6 +1178,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene for (VaultEntry entry : _selectedEntries) { GoogleAuthInfo authInfo = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer()); authInfos.add(authInfo); + + _auditLogRepository.addEntrySharedEvent(entry.getUUID().toString()); } intent.putExtra("authInfos", authInfos); diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/AuditLogPreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/AuditLogPreferencesFragment.java new file mode 100644 index 00000000..cabbbd61 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/AuditLogPreferencesFragment.java @@ -0,0 +1,97 @@ +package com.beemdevelopment.aegis.ui.fragments.preferences; + +import android.graphics.Rect; +import android.os.Bundle; +import android.view.View; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.beemdevelopment.aegis.R; +import com.beemdevelopment.aegis.database.AuditLogEntry; +import com.beemdevelopment.aegis.database.AuditLogRepository; +import com.beemdevelopment.aegis.helpers.MetricsHelper; +import com.beemdevelopment.aegis.ui.models.AuditLogEntryModel; +import com.beemdevelopment.aegis.ui.views.AuditLogAdapter; +import com.beemdevelopment.aegis.vault.VaultEntry; +import com.beemdevelopment.aegis.vault.VaultManager; + +import java.util.List; +import java.util.UUID; + +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; + +@AndroidEntryPoint +public class AuditLogPreferencesFragment extends Fragment { + + @Inject + AuditLogRepository _auditLogRepository; + + private AuditLogAdapter _adapter; + + private RecyclerView _auditLogRecyclerView; + private LinearLayout _noAuditLogsView; + + @Inject + VaultManager _vaultManager; + + public AuditLogPreferencesFragment() { + super(R.layout.fragment_audit_log); + } + + @Override + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + LiveData> entries = _auditLogRepository.getAllAuditLogEntries(); + + _adapter = new AuditLogAdapter(); + _noAuditLogsView = view.findViewById(R.id.vEmptyList); + _auditLogRecyclerView = view.findViewById(R.id.list_audit_log); + LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext()); + _auditLogRecyclerView.addItemDecoration(new SpacesItemDecoration(8)); + _auditLogRecyclerView.setLayoutManager(layoutManager); + _auditLogRecyclerView.setAdapter(_adapter); + _auditLogRecyclerView.setNestedScrollingEnabled(false); + + entries.observe(getViewLifecycleOwner(), entries1 -> { + _noAuditLogsView.setVisibility(entries1.isEmpty() ? View.VISIBLE : View.GONE); + + for (AuditLogEntry entry : entries1) { + + VaultEntry referencedEntry = null; + if (entry.getReference() != null) { + referencedEntry = _vaultManager.getVault().getEntryByUUID(UUID.fromString(entry.getReference())); + } + + AuditLogEntryModel auditLogEntryModel = new AuditLogEntryModel(entry, referencedEntry); + _adapter.addAuditLogEntryModel(auditLogEntryModel); + } + }); + } + + private class SpacesItemDecoration extends RecyclerView.ItemDecoration { + private final int _space; + + public SpacesItemDecoration(int dpSpace) { + _space = MetricsHelper.convertDpToPixels(getContext(), dpSpace); + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + outRect.left = _space; + outRect.right = _space; + outRect.bottom = _space; + + if (parent.getChildLayoutPosition(view) == 0) { + outRect.top = _space; + } + } + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java index acd8188d..fb507840 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/ImportExportPreferencesFragment.java @@ -23,6 +23,7 @@ import androidx.preference.Preference; import com.beemdevelopment.aegis.BuildConfig; import com.beemdevelopment.aegis.R; +import com.beemdevelopment.aegis.database.AuditLogRepository; import com.beemdevelopment.aegis.helpers.DropdownHelper; import com.beemdevelopment.aegis.importers.DatabaseImporter; import com.beemdevelopment.aegis.otp.GoogleAuthInfo; @@ -63,6 +64,7 @@ import java.util.UUID; import java.util.stream.Collectors; import javax.crypto.Cipher; +import javax.inject.Inject; public class ImportExportPreferencesFragment extends PreferencesFragment { // keep a reference to the type of database converter that was selected @@ -277,6 +279,7 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { .setType(getExportMimeType(getExportRequestCode(pos, encrypt))) .putExtra(Intent.EXTRA_TITLE, fileInfo.toString()); + _auditLogRepository.addVaultExportedEvent(); ActivityResultLauncher resultLauncher = getExportRequestLauncher(pos, encrypt); _vaultManager.fireIntentLauncher(this, intent, resultLauncher); }); @@ -320,7 +323,7 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { // if the user creates an export, hide the backup reminder _prefs.setLatestExportTimeNow(); - + _auditLogRepository.addVaultExportedEvent(); Uri uri = FileProvider.getUriForFile(requireContext(), BuildConfig.FILE_PROVIDER_AUTHORITY, file); Intent intent = new Intent(Intent.ACTION_SEND) .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) @@ -559,7 +562,7 @@ public class ImportExportPreferencesFragment extends PreferencesFragment { } else { // if the user creates an export, hide the backup reminder _prefs.setLatestExportTimeNow(); - + _auditLogRepository.addVaultExportedEvent(); Toast.makeText(requireContext(), getString(R.string.exported_vault), Toast.LENGTH_SHORT).show(); } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/PreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/PreferencesFragment.java index 992400a7..7968ff1e 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/PreferencesFragment.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/preferences/PreferencesFragment.java @@ -13,6 +13,7 @@ import androidx.preference.PreferenceFragmentCompat; import com.beemdevelopment.aegis.Preferences; import com.beemdevelopment.aegis.R; +import com.beemdevelopment.aegis.database.AuditLogRepository; import com.beemdevelopment.aegis.helpers.AnimationsHelper; import com.beemdevelopment.aegis.ui.dialogs.Dialogs; import com.beemdevelopment.aegis.vault.VaultManager; @@ -38,6 +39,9 @@ public abstract class PreferencesFragment extends PreferenceFragmentCompat { @Inject VaultManager _vaultManager; + @Inject + protected AuditLogRepository _auditLogRepository; + @Override @CallSuper public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/models/AuditLogEntryModel.java b/app/src/main/java/com/beemdevelopment/aegis/ui/models/AuditLogEntryModel.java new file mode 100644 index 00000000..a020d3a0 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/models/AuditLogEntryModel.java @@ -0,0 +1,24 @@ +package com.beemdevelopment.aegis.ui.models; + +import com.beemdevelopment.aegis.database.AuditLogEntry; +import com.beemdevelopment.aegis.vault.VaultEntry; + +import javax.annotation.Nullable; + +public class AuditLogEntryModel { + private AuditLogEntry _auditLogEntry; + private VaultEntry _referencedVaultEntry; + + public AuditLogEntryModel(AuditLogEntry auditLogEntry, @Nullable VaultEntry referencedVaultEntry) { + _auditLogEntry = auditLogEntry; + _referencedVaultEntry = referencedVaultEntry; + } + + public AuditLogEntry getAuditLogEntry() { + return _auditLogEntry; + } + + public VaultEntry getReferencedVaultEntry() { + return _referencedVaultEntry; + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/views/AuditLogAdapter.java b/app/src/main/java/com/beemdevelopment/aegis/ui/views/AuditLogAdapter.java new file mode 100644 index 00000000..a2950d37 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/views/AuditLogAdapter.java @@ -0,0 +1,62 @@ +package com.beemdevelopment.aegis.ui.views; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.beemdevelopment.aegis.R; +import com.beemdevelopment.aegis.database.AuditLogEntry; +import com.beemdevelopment.aegis.ui.models.AuditLogEntryModel; +import com.beemdevelopment.aegis.vault.VaultEntry; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class AuditLogAdapter extends RecyclerView.Adapter { + private List _auditLogEntryModels; + private List _referencedEntries; + + public AuditLogAdapter() { + _auditLogEntryModels = new ArrayList<>(); + _referencedEntries = new ArrayList<>(); + } + + public void addAuditLogEntryModel(AuditLogEntryModel auditLogEntryModel) { + _auditLogEntryModels.add(auditLogEntryModel); + + int position = getItemCount() - 1; + if (position == 0) { + notifyDataSetChanged(); + } else { + notifyItemInserted(position); + } + } + + public void addReferencedEntry(VaultEntry vaultEntry) { + _referencedEntries.add(vaultEntry); + } + + @NonNull + @Override + public AuditLogHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_audit_log, parent, false); + return new AuditLogHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull AuditLogHolder holder, int position) { + AuditLogEntryModel auditLogEntryModel = _auditLogEntryModels.get(position); + + VaultEntry referencedEntry = null; + holder.setData(auditLogEntryModel); + } + + @Override + public int getItemCount() { + return _auditLogEntryModels.size(); + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/views/AuditLogHolder.java b/app/src/main/java/com/beemdevelopment/aegis/ui/views/AuditLogHolder.java new file mode 100644 index 00000000..8ff088a2 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/views/AuditLogHolder.java @@ -0,0 +1,140 @@ +package com.beemdevelopment.aegis.ui.views; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.cardview.widget.CardView; +import androidx.recyclerview.widget.RecyclerView; + +import com.beemdevelopment.aegis.EventType; +import com.beemdevelopment.aegis.R; +import com.beemdevelopment.aegis.database.AuditLogEntry; +import com.beemdevelopment.aegis.ui.models.AuditLogEntryModel; +import com.beemdevelopment.aegis.vault.VaultEntry; +import com.google.android.material.color.MaterialColors; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; + +public class AuditLogHolder extends RecyclerView.ViewHolder { + + private final CardView _cardView; + private final ImageView _auditLogEntryIcon; + private final TextView _auditLogEntryTitle; + private final TextView _auditLogEntryDescription; + private final TextView _auditLogEntryReference; + private final TextView _auditLogEntryTimestamp; + private final int _errorBackgroundColor; + private final ColorStateList _initialBackgroundColor; + private final int _initialIconColor; + + public AuditLogHolder(final View view) { + super(view); + + _cardView = (CardView)view; + _auditLogEntryIcon = view.findViewById(R.id.iv_icon_view); + _auditLogEntryTitle = view.findViewById(R.id.text_audit_log_title); + _auditLogEntryDescription = view.findViewById(R.id.text_audit_log_description); + _auditLogEntryReference = view.findViewById(R.id.text_audit_log_reference); + _auditLogEntryTimestamp = view.findViewById(R.id.text_audit_log_timestamp); + + _initialBackgroundColor = _cardView.getCardBackgroundColor(); + _initialIconColor = MaterialColors.getColor(view, com.google.android.material.R.attr.colorTertiaryContainer); + _errorBackgroundColor = MaterialColors.getColor(view, com.google.android.material.R.attr.colorErrorContainer); + } + + public void setData(AuditLogEntryModel auditLogEntryModel) { + AuditLogEntry auditLogEntry = auditLogEntryModel.getAuditLogEntry(); + _auditLogEntryIcon.setImageResource(getIconResource(auditLogEntry.getEventType())); + _auditLogEntryTitle.setText(EventType.getEventTitleRes(auditLogEntry.getEventType())); + _auditLogEntryDescription.setText(getEventTypeDescriptionRes(auditLogEntry.getEventType())); + + _auditLogEntryTimestamp.setText(formatTimestamp(_cardView.getContext(), auditLogEntry.getTimestamp()).toLowerCase()); + + if (auditLogEntryModel.getReferencedVaultEntry() != null) { + VaultEntry referencedVaultEntry = auditLogEntryModel.getReferencedVaultEntry(); + _auditLogEntryReference.setText(String.format("%s (%s)", referencedVaultEntry.getIssuer(), referencedVaultEntry.getName())); + } else { + _auditLogEntryReference.setVisibility(View.GONE); + } + + setCardBackgroundColor(auditLogEntry.getEventType()); + } + + private void setCardBackgroundColor(EventType eventType) { + if (eventType == EventType.VAULT_UNLOCK_FAILED_PASSWORD || eventType == EventType.VAULT_UNLOCK_FAILED_BIOMETRICS) { + _cardView.setCardBackgroundColor(_errorBackgroundColor); + _auditLogEntryIcon.setBackgroundColor(_errorBackgroundColor); + } else { + _cardView.setCardBackgroundColor(_initialBackgroundColor); + _auditLogEntryIcon.setBackgroundColor(_initialIconColor); + } + } + + private int getEventTypeDescriptionRes(EventType eventType) { + switch (eventType) { + case VAULT_UNLOCKED: + return R.string.event_description_vault_unlocked; + case VAULT_BACKUP_CREATED: + return R.string.event_description_backup_created; + case VAULT_ANDROID_BACKUP_CREATED: + return R.string.event_description_android_backup_created; + case VAULT_EXPORTED: + return R.string.event_description_vault_exported; + case ENTRY_SHARED: + return R.string.event_description_entry_shared; + case VAULT_UNLOCK_FAILED_PASSWORD: + return R.string.event_description_vault_unlock_failed_password; + case VAULT_UNLOCK_FAILED_BIOMETRICS: + return R.string.event_description_vault_unlock_failed_biometrics; + default: + return R.string.event_unknown; + } + } + + private int getIconResource(EventType eventType) { + switch(eventType) { + case VAULT_UNLOCKED: + return R.drawable.ic_lock_open; + case VAULT_BACKUP_CREATED: + case VAULT_ANDROID_BACKUP_CREATED: + return R.drawable.ic_folder_zip; + case VAULT_EXPORTED: + return R.drawable.ic_export_notes; + case ENTRY_SHARED: + return R.drawable.ic_share; + case VAULT_UNLOCK_FAILED_PASSWORD: + case VAULT_UNLOCK_FAILED_BIOMETRICS: + return R.drawable.ic_lock; + } + + return -1; + } + + private static String formatTimestamp(Context context, long epochMilli) { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime timestamp = LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMilli), ZoneId.systemDefault()); + + long daysBetween = ChronoUnit.DAYS.between(timestamp.toLocalDate(), now.toLocalDate()); + DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("h:mm a"); + + if (daysBetween < 1) { + String formattedTime = timestamp.format(timeFormatter); + return context.getString(R.string.today_at_time, formattedTime); + } else if (daysBetween < 7) { + DateTimeFormatter dayOfWeekFormatter = DateTimeFormatter.ofPattern("EEEE"); + String dayOfWeek = timestamp.format(dayOfWeekFormatter); + String formattedTime = timestamp.format(timeFormatter); + return context.getString(R.string.day_of_week_at_time, dayOfWeek, formattedTime); + } else { + DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(context.getString(R.string.date_format)); + return timestamp.format(dateFormatter); + } + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultBackupManager.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultBackupManager.java index 48711626..46d515c1 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultBackupManager.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultBackupManager.java @@ -10,6 +10,7 @@ import androidx.annotation.NonNull; import androidx.documentfile.provider.DocumentFile; import com.beemdevelopment.aegis.Preferences; +import com.beemdevelopment.aegis.database.AuditLogRepository; import com.beemdevelopment.aegis.util.IOUtils; import java.io.File; @@ -41,17 +42,20 @@ public class VaultBackupManager { private final Context _context; private final Preferences _prefs; private final ExecutorService _executor; + private final AuditLogRepository _auditLogRepository; - public VaultBackupManager(Context context) { + public VaultBackupManager(Context context, AuditLogRepository auditLogRepository) { _context = context; _prefs = new Preferences(context); _executor = Executors.newSingleThreadExecutor(); + _auditLogRepository = auditLogRepository; } public void scheduleBackup(File tempFile, Uri dirUri, int versionsToKeep) { _executor.execute(() -> { try { createBackup(tempFile, dirUri, versionsToKeep); + _auditLogRepository.addBackupCreatedEvent(); _prefs.setBuiltInBackupResult(new Preferences.BackupResult(null)); } catch (VaultRepositoryException | VaultBackupPermissionException e) { e.printStackTrace(); diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java index c9191424..a972ea77 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java +++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java @@ -15,6 +15,7 @@ import com.beemdevelopment.aegis.Preferences; import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.crypto.KeyStoreHandle; import com.beemdevelopment.aegis.crypto.KeyStoreHandleException; +import com.beemdevelopment.aegis.database.AuditLogRepository; import com.beemdevelopment.aegis.services.NotificationService; import com.beemdevelopment.aegis.ui.dialogs.Dialogs; @@ -37,12 +38,15 @@ public class VaultManager { private final List _lockListeners; private boolean _blockAutoLock; - public VaultManager(@NonNull Context context) { + private final AuditLogRepository _auditLogRepository; + + public VaultManager(@NonNull Context context, AuditLogRepository auditLogRepository) { _context = context; _prefs = new Preferences(_context); - _backups = new VaultBackupManager(_context); + _backups = new VaultBackupManager(_context, auditLogRepository); _androidBackups = new BackupManager(context); _lockListeners = new ArrayList<>(); + _auditLogRepository = auditLogRepository; } /** diff --git a/app/src/main/res/drawable/ic_export_notes.xml b/app/src/main/res/drawable/ic_export_notes.xml new file mode 100644 index 00000000..839aae1d --- /dev/null +++ b/app/src/main/res/drawable/ic_export_notes.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_folder_zip.xml b/app/src/main/res/drawable/ic_folder_zip.xml new file mode 100644 index 00000000..aab56560 --- /dev/null +++ b/app/src/main/res/drawable/ic_folder_zip.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_lock.xml b/app/src/main/res/drawable/ic_lock.xml new file mode 100644 index 00000000..96617192 --- /dev/null +++ b/app/src/main/res/drawable/ic_lock.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_lock_open.xml b/app/src/main/res/drawable/ic_lock_open.xml new file mode 100644 index 00000000..e2cec03e --- /dev/null +++ b/app/src/main/res/drawable/ic_lock_open.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml new file mode 100644 index 00000000..27610432 --- /dev/null +++ b/app/src/main/res/drawable/ic_share.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_timeline_24.xml b/app/src/main/res/drawable/ic_timeline_24.xml new file mode 100644 index 00000000..1657a3a7 --- /dev/null +++ b/app/src/main/res/drawable/ic_timeline_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/card_audit_log.xml b/app/src/main/res/layout/card_audit_log.xml new file mode 100644 index 00000000..836efbfd --- /dev/null +++ b/app/src/main/res/layout/card_audit_log.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_audit_log.xml b/app/src/main/res/layout/fragment_audit_log.xml new file mode 100644 index 00000000..d10feaad --- /dev/null +++ b/app/src/main/res/layout/fragment_audit_log.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 846b15b4..6c96319a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,6 +31,8 @@ Configure encryption, biometric unlock, auto lock and other security settings. Import & Export Import backups of Aegis or other authenticator apps. Create manual exports of your Aegis vault. + Audit log + Find a list of all reported important events that happened within the app. Backups Set up automatic backups to a location of your choosing or enable participation in Android\'s cloud backup system. Icon packs @@ -106,6 +108,28 @@ Change password Set a new password which you will need to unlock your vault + No reported events + No important events have been reported within the app + Vault unlocked + The vault has been successfully unlocked + Backup created + A backup of the vault has been successfully created + Backup created by Android + A backup of the vault has been successfully created by Android + Vault exported + A copy of the vault has been exported + Entry shared + An entry was shared + Vault unlock failed (password) + An attempt to unlock the vault with a password failed + Vault unlock failed (biometrics) + An attempt to unlock the vault with biometrics failed + Unknown event type + + Today at %1$s + %1$s at %2$s + dd/MM/yyyy + Encrypt the vault This action will export the vault out of Aegis\' internal storage. Select the format you\'d like your export to be in: You are about to export an unencrypted copy of your Aegis vault. This is not recommended. diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 1f399dea..f743089a 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -258,4 +258,9 @@ + + diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 9402b3ab..b893fcce 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -40,4 +40,10 @@ app:title="@string/pref_section_import_export_title" app:summary="@string/pref_section_import_export_summary" /> + +