Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">

<uses-feature
android:name="android.hardware.camera"
Expand All @@ -9,13 +10,15 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<queries>
<package android:name="com.nextcloud.client" />
<package android:name="com.nextcloud.android.beta" />
</queries>

<application
android:name=".PassmanApp"
android:allowBackup="false"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
Expand Down Expand Up @@ -47,13 +50,15 @@
android:theme="@android:style/Theme.NoDisplay" />
<activity
android:name=".activities.AutofillInteractionActivity"
android:theme="@style/AppTheme.NoActionBar"/>
android:theme="@style/AppTheme.NoActionBar"
tools:targetApi="26" />

<service
android:name=".autofill.CredentialAutofillService"
android:exported="false"
android:label="Passman Credential Autofill Service"
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
android:permission="android.permission.BIND_AUTOFILL_SERVICE"
tools:targetApi="26">
<meta-data
android:name="android.autofill"
android:resource="@xml/autofill_service" />
Expand Down
55 changes: 55 additions & 0 deletions app/src/main/java/es/wolfi/app/passman/PassmanApp.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package es.wolfi.app.passman;

import android.app.Activity;
import android.app.Application;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class PassmanApp extends Application {
private Activity currentActivity;

@Override
public void onCreate() {
super.onCreate();
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {}

@Override
public void onActivityStarted(@NonNull Activity activity) {
currentActivity = activity;
}

@Override
public void onActivityResumed(@NonNull Activity activity) {
currentActivity = activity;
}

@Override
public void onActivityPaused(@NonNull Activity activity) {
if (currentActivity == activity) {
currentActivity = null;
}
}

@Override
public void onActivityStopped(@NonNull Activity activity) {}

@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {}

@Override
public void onActivityDestroyed(@NonNull Activity activity) {
if (currentActivity == activity) {
currentActivity = null;
}
}
});
}

public Activity getCurrentActivity() {
return currentActivity;
}
}
4 changes: 3 additions & 1 deletion app/src/main/java/es/wolfi/app/passman/SettingValues.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ public enum SettingValues {
KEY_STORE_ENCRYPTION_KEY("key_store_encryption_key"),
CREDENTIAL_LABEL_SORT("credential_label_sort"),
CASE_INSENSITIVE_CREDENTIAL_LABEL_SORT("case_insensitive_credential_label_sort"),
RESTORE_CUSTOM_CREDENTIAL_SORT_ORDER("restore_custom_credential_sort_order");
RESTORE_CUSTOM_CREDENTIAL_SORT_ORDER("restore_custom_credential_sort_order"),
VAULT_AUTO_LOCK_DELAY("vault_auto_lock_delay"),
ENABLE_SCREENSHOT_PROTECTION("enable_screenshot_protection");

private final String name;

Expand Down
174 changes: 174 additions & 0 deletions app/src/main/java/es/wolfi/app/passman/VaultLockManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package es.wolfi.app.passman;

import android.app.Activity;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;

import es.wolfi.app.passman.activities.BaseActivity;
import es.wolfi.app.passman.activities.PasswordListActivity;
import es.wolfi.passman.API.Vault;

public class VaultLockManager {
private static final String TAG = "VaultLockManager";
private static final String CHANNEL_ID = "vault_security";
private static final int NOTIFICATION_ID = 1001;

private static VaultLockManager instance;
private final Handler handler = new Handler(Looper.getMainLooper());
private Runnable lockRunnable;
private Runnable countdownRunnable;
private int timeoutMinutes = 0;
private long lastInteractionTime = 0;
private final PassmanApp passmanApp;

private VaultLockManager(PassmanApp passmanApp) {
this.passmanApp = passmanApp;
createNotificationChannel();
}

public static synchronized VaultLockManager getInstance(PassmanApp passmanApp) {
if (instance == null) {
instance = new VaultLockManager(passmanApp);
}
return instance;
}

private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = passmanApp.getString(R.string.security_channel_name);
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
NotificationManager notificationManager = passmanApp.getSystemService(NotificationManager.class);
if (notificationManager != null) {
notificationManager.createNotificationChannel(channel);
}
}
}

public void updateConfig(int minutes) {
this.timeoutMinutes = minutes;
resetTimer();
}

public void resetTimer() {
lastInteractionTime = System.currentTimeMillis();

handler.removeCallbacks(lockRunnable);
handler.removeCallbacks(countdownRunnable);

hideOverlayOnCurrentActivity();

if (timeoutMinutes <= 0) {
return;
}

long totalDelayMillis = (long) timeoutMinutes * 60 * 1000;
long countdownStartMillis = totalDelayMillis - 3000;

if (totalDelayMillis <= 3000) {
// If timeout is very short, just lock without countdown for now to avoid complexity
lockRunnable = this::lockActiveVault;
handler.postDelayed(lockRunnable, totalDelayMillis);
} else {
countdownRunnable = () -> startCountdown(3);
handler.postDelayed(countdownRunnable, countdownStartMillis);
}
}

public void checkLockOnResume() {
if (timeoutMinutes <= 0) return;

long currentTime = System.currentTimeMillis();
long elapsedMillis = currentTime - lastInteractionTime;
long timeoutMillis = (long) timeoutMinutes * 60 * 1000;

if (elapsedMillis >= timeoutMillis) {
Log.d(TAG, "Lock timeout exceeded during background/pause, locking now");
lockActiveVault();
} else {
// Recalculate remaining time and reschedule
resetTimer();
// Adjust the timer to account for elapsed time
handler.removeCallbacks(lockRunnable);
handler.removeCallbacks(countdownRunnable);

long remainingMillis = timeoutMillis - elapsedMillis;
if (remainingMillis <= 3000) {
lockRunnable = this::lockActiveVault;
handler.postDelayed(lockRunnable, remainingMillis);
} else {
countdownRunnable = () -> startCountdown(3);
handler.postDelayed(countdownRunnable, remainingMillis - 3000);
}
}
}

private void startCountdown(int seconds) {
if (seconds <= 0) {
lockActiveVault();
return;
}

showOverlayOnCurrentActivity(seconds);

countdownRunnable = () -> startCountdown(seconds - 1);
handler.postDelayed(countdownRunnable, 1000);
}

private void showOverlayOnCurrentActivity(int seconds) {
Activity currentActivity = passmanApp.getCurrentActivity();
if (currentActivity instanceof BaseActivity baseActivity) {
baseActivity.showCountdownOverlay(seconds);
}
}

private void hideOverlayOnCurrentActivity() {
Activity currentActivity = passmanApp.getCurrentActivity();
if (currentActivity instanceof BaseActivity baseActivity) {
baseActivity.hideCountdownOverlay();
}
}

public void lockActiveVault() {
hideOverlayOnCurrentActivity();

SingleTon ton = SingleTon.getTon();
Vault vault = (Vault) ton.getExtra(SettingValues.ACTIVE_VAULT.toString());
if (vault != null && vault.is_unlocked()) {
Log.d(TAG, "Locking active vault");
vault.lock();
ton.addExtra(SettingValues.ACTIVE_VAULT.toString(), vault);
ton.addExtra(vault.guid, vault);

postLockedNotification();

Activity currentActivity = passmanApp.getCurrentActivity();
if (currentActivity instanceof PasswordListActivity passwordListActivity) {
passwordListActivity.runOnUiThread(passwordListActivity::lockVault);
}
}
}

private void postLockedNotification() {
NotificationCompat.Builder builder = new NotificationCompat.Builder(passmanApp, CHANNEL_ID)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(passmanApp.getString(R.string.vault_locked_notification_title))
.setContentText(passmanApp.getString(R.string.vault_locked_notification_text))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true);

NotificationManagerCompat notificationManager = NotificationManagerCompat.from(passmanApp);
try {
notificationManager.notify(NOTIFICATION_ID, builder.build());
} catch (SecurityException e) {
Log.e(TAG, "Notification permission missing", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
import android.widget.Toast;

import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;

import java.util.ArrayList;
import java.util.Set;
Expand All @@ -51,7 +50,7 @@
import es.wolfi.passman.API.Vault;

@RequiresApi(api = Build.VERSION_CODES.O)
public class AutofillInteractionActivity extends AppCompatActivity implements
public class AutofillInteractionActivity extends BaseActivity implements
VaultLockScreenFragment.VaultUnlockInteractionListener,
CredentialItemFragment.OnListFragmentInteractionListener {
public final static String LOG_TAG = "AutofillInteractionAct.";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package es.wolfi.app.passman.activities;

import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.TextView;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

import es.wolfi.app.passman.PassmanApp;
import es.wolfi.app.passman.R;
import es.wolfi.app.passman.SettingValues;
import es.wolfi.app.passman.VaultLockManager;

public abstract class BaseActivity extends AppCompatActivity {
private View countdownOverlay;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
applyScreenshotProtection();
}

@Override
public void onUserInteraction() {
super.onUserInteraction();
hideCountdownOverlay();
VaultLockManager.getInstance((PassmanApp) getApplication()).resetTimer();
}

@Override
protected void onResume() {
super.onResume();
applyScreenshotProtection();
VaultLockManager.getInstance((PassmanApp) getApplication()).checkLockOnResume();
}

public void applyScreenshotProtection() {
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
boolean enabled = settings.getBoolean(SettingValues.ENABLE_SCREENSHOT_PROTECTION.toString(), true);
if (enabled) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
}

public void showCountdownOverlay(int secondsLeft) {
runOnUiThread(() -> {
if (countdownOverlay == null) {
countdownOverlay = LayoutInflater.from(this).inflate(R.layout.layout_lock_countdown, null);
addContentView(
countdownOverlay,
new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
);
}
countdownOverlay.setVisibility(View.VISIBLE);
TextView textView = countdownOverlay.findViewById(R.id.lock_countdown_text);
textView.setText(getString(R.string.vault_locking_in, secondsLeft));
});
}

public void hideCountdownOverlay() {
runOnUiThread(() -> {
if (countdownOverlay != null) {
countdownOverlay.setVisibility(View.GONE);
}
});
}
}
Loading