Skip to content

Commit 0849c6f

Browse files
committed
add duress password management UI
1 parent 4ce48a8 commit 0849c6f

File tree

8 files changed

+575
-0
lines changed

8 files changed

+575
-0
lines changed

AndroidManifest.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2737,6 +2737,16 @@
27372737
<meta-data android:name="com.android.settings.icon_tintable" android:value="true" />
27382738
</activity>
27392739

2740+
<activity android:name=".security.DuressPasswordMainActivity"
2741+
android:excludeFromRecents="true"
2742+
android:theme="@style/GlifTheme.Light"
2743+
android:exported="false" />
2744+
2745+
<activity android:name=".security.DuressPasswordSetupActivity"
2746+
android:excludeFromRecents="true"
2747+
android:theme="@style/GlifTheme.Light"
2748+
android:exported="false" />
2749+
27402750
<activity android:name=".biometrics.fingerprint.FingerprintEnrollSuggestionActivity"
27412751
android:exported="true"
27422752
android:icon="@drawable/ic_suggestion_fingerprint">
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
3+
<com.google.android.setupdesign.GlifLayout
4+
xmlns:android="http://schemas.android.com/apk/res/android"
5+
xmlns:app="http://schemas.android.com/apk/res-auto"
6+
android:id="@+id/glif_layout"
7+
android:layout_width="match_parent"
8+
android:layout_height="match_parent"
9+
android:icon="@drawable/ic_lock"
10+
>
11+
<LinearLayout
12+
android:layout_width="match_parent"
13+
android:layout_height="match_parent"
14+
android:orientation="vertical"
15+
style="@style/SudContentFrame">
16+
17+
<TextView
18+
android:layout_width="match_parent"
19+
android:layout_height="wrap_content"
20+
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
21+
android:text="@string/unlock_set_unlock_pin_title" />
22+
23+
<EditText
24+
android:id="@+id/pin_input"
25+
android:layout_width="match_parent"
26+
android:layout_height="wrap_content"
27+
android:layout_marginTop="16dp"
28+
android:hint="@string/duress_pwd_enter_pin"
29+
android:imeOptions="actionNext"
30+
android:inputType="numberPassword" />
31+
32+
<EditText
33+
android:id="@+id/pin_input_confirmation"
34+
android:layout_width="match_parent"
35+
android:layout_height="wrap_content"
36+
android:layout_marginTop="16dp"
37+
android:hint="@string/duress_pwd_confirm_pin"
38+
android:imeOptions="actionNext"
39+
android:inputType="numberPassword" />
40+
41+
<TextView
42+
android:layout_width="match_parent"
43+
android:layout_height="wrap_content"
44+
android:layout_marginTop="32dp"
45+
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
46+
android:text="@string/unlock_set_unlock_password_title" />
47+
48+
<EditText
49+
android:id="@+id/password_input"
50+
android:layout_width="match_parent"
51+
android:layout_height="wrap_content"
52+
android:layout_marginTop="16dp"
53+
android:hint="@string/duress_pwd_enter_password"
54+
android:imeOptions="actionNext"
55+
android:inputType="textPassword" />
56+
57+
<EditText
58+
android:id="@+id/password_input_confirmation"
59+
android:layout_width="match_parent"
60+
android:layout_height="wrap_content"
61+
android:layout_marginTop="16dp"
62+
android:hint="@string/duress_pwd_confirm_password"
63+
android:imeOptions="actionDone"
64+
android:inputType="textPassword" />
65+
66+
</LinearLayout>
67+
</com.google.android.setupdesign.GlifLayout>

res/values/strings_ext.xml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,5 +159,32 @@ Native code debugging (ptrace) slightly weakens the app sandbox. Some apps (most
159159
<string name="usbc_port_charging_only_when_locked_afu_title">Charging-only when locked, except before first unlock</string>
160160
<string name="usbc_port_charging_only_when_locked_afu_summary">Doesn’t apply to connections that were made before locking or first unlock</string>
161161
<string name="usbc_port_on_title">On</string>
162+
163+
<string name="duress_pwd_pref_title">Duress password</string>
164+
<string name="duress_pwd_description">"Duress password can be entered instead of the user password in any place that asks for it, such as the lock screen of any user and any user or work profile password prompt.
165+
166+
Submitting the duress password will immediately make the storage contents permanently inaccessible, delete all eSIMs and then power off the device. GrapheneOS will remain installed.
167+
168+
Duress PIN works the same way duress password does."</string>
169+
<string name="duress_pwd_enter_pin">Enter duress PIN</string>
170+
<string name="duress_pwd_confirm_pin">Confirm duress PIN</string>
171+
<string name="duress_pwd_enter_password">Enter duress password</string>
172+
<string name="duress_pwd_confirm_password">Confirm duress password</string>
173+
<string name="duress_pwd_action_add">Add duress PIN and password</string>
174+
<string name="duress_pwd_add_button">Add</string>
175+
<string name="duress_pwd_toast_added">Added duress PIN and password</string>
176+
<string name="duress_pwd_toast_updated">Updated duress PIN and password</string>
177+
<string name="duress_pwd_update_button">Update</string>
178+
<string name="duress_pwd_cancel_button">Cancel</string>
179+
<string name="duress_pwd_proceed_button">Proceed</string>
180+
<string name="duress_pwd_delete_button">Delete</string>
181+
<string name="duress_pwd_action_update">Update duress PIN and password</string>
182+
<string name="duress_pwd_action_delete">Delete duress PIN and password</string>
183+
<string name="duress_pwd_action_delete_confirmation">Delete duress PIN and password?</string>
184+
<string name="duress_pwd_save_warning_title">WARNING</string>
185+
<string name="duress_pwd_save_warning_text">"Submitting the duress PIN/password instead of the user PIN/password <b>will immediately make the storage contents permanently inaccessible</b> and will delete all eSIMs."</string>
186+
<string name="duress_pwd_save_error">Duress PIN and password were not saved.\n\nDetails:\n%1$s</string>
187+
<string name="duress_pwd_delete_error">Duress PIN and password were not deleted.\n\nDetails:\n%1$s</string>
188+
<string name="duress_pwd_error_dialog_dismiss">Dismiss</string>
162189

163190
</resources>

res/xml/security_dashboard_settings.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@
5454
android:title="@string/auto_reboot_title"
5555
settings:controller="com.android.settings.security.AutoRebootPrefController" />
5656

57+
<Preference
58+
android:key="duress_password"
59+
android:title="@string/duress_pwd_pref_title"
60+
settings:controller="com.android.settings.security.DuressPasswordPrefController" />
61+
5762
<Preference
5863
android:key="deny_new_usb"
5964
android:title="@string/deny_new_usb_title"
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package com.android.settings.security;
2+
3+
import android.app.Activity;
4+
import android.os.Bundle;
5+
import android.view.WindowManager;
6+
7+
import androidx.annotation.Nullable;
8+
9+
import com.android.internal.widget.LockPatternUtils;
10+
import com.android.settings.SetupWizardUtils;
11+
import com.android.settings.overlay.FeatureFactory;
12+
import com.google.android.setupdesign.GlifLayout;
13+
import com.google.android.setupdesign.template.DescriptionMixin;
14+
import com.google.android.setupdesign.util.ThemeHelper;
15+
16+
public class DuressPasswordActivity extends Activity {
17+
18+
@Override
19+
protected void onCreate(@Nullable Bundle savedInstanceState) {
20+
super.onCreate(savedInstanceState);
21+
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
22+
23+
setTheme(SetupWizardUtils.getTheme(this, getIntent()));
24+
ThemeHelper.trySetDynamicColor(this);
25+
26+
setResult(RESULT_OK);
27+
}
28+
29+
protected boolean allowNextOnStop;
30+
31+
@Override
32+
protected void onStop() {
33+
super.onStop();
34+
if (!isChangingConfigurations() && hasUserCredential()) {
35+
if (allowNextOnStop) {
36+
allowNextOnStop = false;
37+
} else {
38+
// require user credential to be re-entered after activity is backgrounded
39+
setResult(RESULT_CANCELED);
40+
finish();
41+
}
42+
}
43+
}
44+
45+
protected boolean hasUserCredential() {
46+
return true;
47+
}
48+
49+
@Override
50+
protected void onDestroy() {
51+
super.onDestroy();
52+
if (!isChangingConfigurations()) {
53+
/** @see com.android.settings.password.ConfirmDeviceCredentialBaseActivity#onDestroy */
54+
getMainThreadHandler().postDelayed(() -> {
55+
System.gc();
56+
System.runFinalization();
57+
System.gc();
58+
}, 5000);
59+
}
60+
}
61+
62+
static void adjustDescriptionStyle(GlifLayout l) {
63+
l.getMixin(DescriptionMixin.class).getTextView().setTextSize(16f);
64+
}
65+
66+
LockPatternUtils getLockPatternUtils() {
67+
return FeatureFactory.getFeatureFactory()
68+
.getSecurityFeatureProvider()
69+
.getLockPatternUtils(this);
70+
}
71+
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package com.android.settings.security;
2+
3+
import android.app.AlertDialog;
4+
import android.content.Intent;
5+
import android.os.Bundle;
6+
import android.util.Log;
7+
8+
import androidx.annotation.DrawableRes;
9+
import androidx.annotation.Nullable;
10+
import androidx.annotation.StringRes;
11+
12+
import com.android.internal.widget.LockPatternUtils;
13+
import com.android.internal.widget.LockscreenCredential;
14+
import com.android.settings.R;
15+
import com.android.settings.password.ChooseLockSettingsHelper;
16+
import com.google.android.setupdesign.GlifRecyclerLayout;
17+
import com.google.android.setupdesign.items.IItem;
18+
import com.google.android.setupdesign.items.Item;
19+
import com.google.android.setupdesign.items.ItemGroup;
20+
import com.google.android.setupdesign.items.RecyclerItemAdapter;
21+
22+
public class DuressPasswordMainActivity extends DuressPasswordActivity implements RecyclerItemAdapter.OnItemSelectedListener {
23+
private static final String TAG = DuressPasswordMainActivity.class.getSimpleName();
24+
private static final String KEY_USER_CREDENTIAL = "user_credential";
25+
26+
private GlifRecyclerLayout layout;
27+
private LockscreenCredential userCredential;
28+
29+
@Override
30+
protected void onCreate(@Nullable Bundle savedInstanceState) {
31+
super.onCreate(savedInstanceState);
32+
33+
var layout = new GlifRecyclerLayout(this);
34+
this.layout = layout;
35+
layout.setIcon(getDrawable(R.drawable.ic_lock));
36+
layout.setHeaderText(R.string.duress_pwd_pref_title);
37+
adjustDescriptionStyle(layout);
38+
layout.setDescriptionText(R.string.duress_pwd_description);
39+
40+
setContentView(layout);
41+
42+
if (savedInstanceState != null) {
43+
userCredential = savedInstanceState.getParcelable(KEY_USER_CREDENTIAL, LockscreenCredential.class);
44+
}
45+
46+
if (userCredential == null) {
47+
userCredential = LockscreenCredential.createNone();
48+
49+
var b = new ChooseLockSettingsHelper.Builder(this);
50+
b.setRequestCode(REQ_CODE_OBTAIN_USER_CREDENTIALS);
51+
b.setReturnCredentials(true);
52+
b.setForegroundOnly(true);
53+
b.show();
54+
}
55+
}
56+
57+
@Override
58+
protected boolean hasUserCredential() {
59+
return !userCredential.isNone();
60+
}
61+
62+
@Override
63+
protected void onSaveInstanceState(Bundle outState) {
64+
super.onSaveInstanceState(outState);
65+
outState.putParcelable(KEY_USER_CREDENTIAL, userCredential);
66+
}
67+
68+
@Override
69+
protected void onResume() {
70+
super.onResume();
71+
updateActionList();
72+
}
73+
74+
private void updateActionList() {
75+
var g = new ItemGroup();
76+
77+
if (getLockPatternUtils().hasDuressCredentials()) {
78+
g.addChild(createItem(R.string.duress_pwd_action_update, R.drawable.ic_edit));
79+
g.addChild(createItem(R.string.duress_pwd_action_delete, R.drawable.ic_delete));
80+
} else {
81+
g.addChild(createItem(R.string.duress_pwd_action_add, R.drawable.ic_add_24dp));
82+
}
83+
84+
var adapter = new RecyclerItemAdapter(g);
85+
adapter.setOnItemSelectedListener(this);
86+
layout.setAdapter(adapter);
87+
}
88+
89+
private Item createItem(@StringRes int title, @DrawableRes int icon) {
90+
var i = new Item();
91+
i.setId(title);
92+
i.setTitle(getText(title));
93+
i.setIcon(getDrawable(icon));
94+
return i;
95+
}
96+
97+
// RecyclerItemAdapter.OnItemSelectedListener
98+
@Override
99+
public void onItemSelected(IItem iitem) {
100+
Item item = (Item) iitem;
101+
int id = item.getId();
102+
103+
if (id == R.string.duress_pwd_action_add || id == R.string.duress_pwd_action_update) {
104+
var i = new Intent(this, DuressPasswordSetupActivity.class);
105+
i.putExtra(DuressPasswordSetupActivity.EXTRA_TITLE_TEXT, id);
106+
i.putExtra(DuressPasswordSetupActivity.EXTRA_USER_CREDENTIAL, userCredential);
107+
allowNextOnStop = true;
108+
startActivityForResult(i, REQ_CODE_SETUP);
109+
} else if (id == R.string.duress_pwd_action_delete) {
110+
var b = new AlertDialog.Builder(this);
111+
b.setMessage(R.string.duress_pwd_action_delete_confirmation);
112+
b.setPositiveButton(R.string.duress_pwd_delete_button, (dialog, which) -> {
113+
LockPatternUtils lpu = getLockPatternUtils();
114+
try {
115+
lpu.deleteDuressCredentials(userCredential);
116+
} catch (Exception e) {
117+
Log.e(TAG, "deleteDuressCredentials failed", e);
118+
119+
var d = new AlertDialog.Builder(this);
120+
d.setMessage(getString(R.string.duress_pwd_delete_error, e.toString()));
121+
d.setNeutralButton(R.string.duress_pwd_error_dialog_dismiss, null);
122+
d.show();
123+
return;
124+
}
125+
updateActionList();
126+
});
127+
b.setNegativeButton(R.string.duress_pwd_cancel_button, null);
128+
b.show();
129+
}
130+
}
131+
132+
static final int REQ_CODE_OBTAIN_USER_CREDENTIALS = 1;
133+
static final int REQ_CODE_SETUP = 2;
134+
135+
@Override
136+
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
137+
if (requestCode == REQ_CODE_SETUP) {
138+
if (resultCode == RESULT_CANCELED) {
139+
finish();
140+
}
141+
return;
142+
}
143+
144+
if (requestCode != REQ_CODE_OBTAIN_USER_CREDENTIALS) {
145+
throw new IllegalStateException(Integer.toString(resultCode));
146+
}
147+
148+
Log.d(TAG, "onActivityResult");
149+
150+
if (resultCode != RESULT_OK) {
151+
finish();
152+
return;
153+
}
154+
155+
if (data == null) {
156+
throw new IllegalStateException("data == null");
157+
}
158+
159+
var credential = data.getParcelableExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, LockscreenCredential.class);
160+
if (credential == null) {
161+
throw new IllegalStateException("no returned credential");
162+
}
163+
userCredential = credential;
164+
}
165+
}

0 commit comments

Comments
 (0)