Skip to content
This repository has been archived by the owner on Jan 6, 2025. It is now read-only.

Commit

Permalink
feat: support password reminder
Browse files Browse the repository at this point in the history
  • Loading branch information
iamhyc committed Nov 22, 2024
1 parent cffd668 commit 2f12e19
Show file tree
Hide file tree
Showing 14 changed files with 337 additions and 13 deletions.
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,6 @@ Src0--"Authenticate"-->Dst2--display-->Dst4

**Release v0.5.0** (2024/11/25)

- [ ] Password Challenge Page Design
- Periodic Notification in the Local Non-Working Hours
- Also allow to skip the challenge
- [ ] Preview Next Token for TOTP-based Schema
- Toggle in Settings Page
- [ ] Support started with `otpauth://` link Want Param
Expand Down
2 changes: 2 additions & 0 deletions entry/src/main/ets/common/conts.ets
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export const PREF_KEY_ITEMS = 'items';
export const PREF_KEY_SETTINGS = 'settings';
export const PREF_MASTER_KEY = 'master-key';
export const PREF_DEC_MASTER_KEY = 'dec-master-key';
export const PREF_PASSWORD_CHALLENGE_TIME = 'password-challenge-timestamp';
export const ONE_DAY_IN_MS = 24*60*60*1000;

export const PREF_ICON_PACKS = 'icon-packs';
export const MAX_ICON_PACK_NUM = 5;
Expand Down
5 changes: 5 additions & 0 deletions entry/src/main/ets/common/events.ets
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ export interface PasswordUpdateRequestSchema {
newPassword?: boolean,
}

export const EVENT_UPDATE_PASSWORD_CHALLENGE_TIME: string = 'password-challenge';
export interface UpdatePasswordChallengeTimeSchema {
timestamp: number,
}

export const EVENT_EXPORT_REQUEST: string = 'export-request';
export interface FileExportRequestSchema {
format: string,
Expand Down
2 changes: 2 additions & 0 deletions entry/src/main/ets/common/settings.ets
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type DigitGroup = '2' | '3' | '4' | 'even' | 'none';
export type ShowAccountName = 'always' | 'smart' | 'off';
export type AuthTypePriority = 'face' | 'touch' | 'pin';
export type ItemSortOrder = 'custom' | 'issuer_asc' | 'issuer_dsc' | 'account_asc' | 'account_dsc' | 'count';
export type PasswordReminderPeriod = 'never' | 'weekly' | 'biweekly' | 'monthly' | 'quarterly';

export const DefaultUserPreferences: UserPreferences = new Map<string, ValueType>([
// general
Expand All @@ -23,6 +24,7 @@ export const DefaultUserPreferences: UserPreferences = new Map<string, ValueType
// icons
['ShowIssuerIcons', true],
// security
['PasswordReminderPeriod', 'biweekly' as PasswordReminderPeriod],
['EnableBiometricUnlock', false],
['AuthTypePriority', 'face' as AuthTypePriority],
['EnableBiometricAuth', false],
Expand Down
108 changes: 104 additions & 4 deletions entry/src/main/ets/components/pages.ets
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { AtlAuthResult } from "../common/schema";
import { LengthUnit } from "@kit.ArkUI";
import { acquireAtlAuth, AuthLevelSupport } from "../crypto/authUtils";
import { AuthTypePriority } from '../common/settings';
import { AtlAuthResult, MasterKeyInfo } from "../common/schema";
import { LengthUnit, promptAction } from "@kit.ArkUI";
import { acquireAtlAuth, AuthLevelSupport, validatePassword } from "../crypto/authUtils";
import { AuthTypePriority, PasswordReminderPeriod } from '../common/settings';
import { EVENT_UPDATE_PASSWORD_CHALLENGE_TIME, UpdatePasswordChallengeTimeSchema } from "../common/events";
import { ONE_DAY_IN_MS, PREF_PASSWORD_CHALLENGE_TIME } from "../common/conts";

@Component
export struct AppLockPage {
Expand Down Expand Up @@ -40,3 +42,101 @@ export struct AppLockPage {
})
}
}

@Component
export struct PasswordChallengePage {
@StorageProp('EncMasterKey') EncMasterKey: MasterKeyInfo = {} as MasterKeyInfo;
@StorageProp(PREF_PASSWORD_CHALLENGE_TIME) PasswordChallengeTime: number = Date.now();
@StorageProp('settingsPasswordReminderPeriod') PasswordReminderPeriod: PasswordReminderPeriod = 'biweekly';
@State password: string = '';

build() {
Stack({ alignContent: Alignment.Bottom }) {
Flex({
direction: FlexDirection.Column,
justifyContent: FlexAlign.Center,
alignItems: ItemAlign.Center,
space: { main:{value:16, unit:LengthUnit.VP} }
}) {
Text() {
SymbolSpan($r('sys.symbol.hand_raised_hexagon_fill'))
}
.fontColor($r('sys.color.font_secondary'))
.fontSize(96)
//
Text($r('app.string.password_challenge_page_title'))
.fontColor($r('sys.color.font_secondary'))
.fontWeight(FontWeight.Medium)
.fontSize(24)
//
Text($r('app.string.password_challenge_page_hint'))
.fontColor($r('sys.color.ohos_id_color_text_secondary'))
.fontSize(16)
.textAlign(TextAlign.Center)
//
TextInput({ placeholder: $r('app.string.setting_setup_password_placeholder'), text: $$this.password })
.width('100%')
.defaultFocus(true)
.type(InputType.Password)
.contentType(ContentType.PASSWORD)
.onSubmit(async () => { await this.verifyPassword(); })
Button($r('app.string.button_text_confirm'))
.width('100%')
.enabled(this.password.length >= 8)
.onClick(async () => { await this.verifyPassword(); })
}
.height('100%')
.width('75%')
//
Text($r('app.string.password_challenge_page_bypass_title', ''))
.fontColor($r('sys.color.brand_font'))
.fontSize(14)
.onClick(() => { this.showPrompt(); })
}
}

aboutToAppear() {
if (this.EncMasterKey.keyAlias===undefined) {
const timestamp = Date.now();
getContext(this).eventHub.emit(EVENT_UPDATE_PASSWORD_CHALLENGE_TIME, {timestamp} as UpdatePasswordChallengeTimeSchema);
}
}

private async verifyPassword() {
if (this.password.length >= 8) {
if ((await validatePassword(this.EncMasterKey, this.password))) {
const timestamp = Date.now();
getContext(this).eventHub.emit(EVENT_UPDATE_PASSWORD_CHALLENGE_TIME, {timestamp} as UpdatePasswordChallengeTimeSchema);
} else {
promptAction.showToast({message:$r('app.string.setting_input_wrong_password'), duration:500});
}
}
}

private showPrompt() {
const _now = Date.now();
let timestamp = this.PasswordChallengeTime + ONE_DAY_IN_MS;
switch (this.PasswordReminderPeriod) {
case 'weekly':
timestamp = _now - (7-1)*ONE_DAY_IN_MS; break;
case 'biweekly':
timestamp = _now - (14-1)*ONE_DAY_IN_MS; break;
case 'monthly':
timestamp = _now - (30-1)*ONE_DAY_IN_MS; break;
case 'quarterly':
timestamp = _now - (90-1)*ONE_DAY_IN_MS; break;
}
AlertDialog.show({
title: $r('app.string.password_challenge_page_bypass_title', '⚠ '),
message: $r('app.string.password_challenge_page_bypass_hint'),
primaryButton: {
value: $r('app.string.button_text_ok'),
defaultFocus: true,
style: DialogButtonStyle.DEFAULT,
action: () => {
getContext(this).eventHub.emit(EVENT_UPDATE_PASSWORD_CHALLENGE_TIME, {timestamp} as UpdatePasswordChallengeTimeSchema);
},
}
})
}
}
22 changes: 21 additions & 1 deletion entry/src/main/ets/entryability/EntryAbility.ets
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import { arrayRearrange, b32decode, isValidBase32String, stringToUint8Array,
Uint8ArrayToString } from '../common/utils';
import { AIGIS_PREF_NAME, FAKE_OTP_CODE,
MAX_ICON_PACK_SIZE,
ONE_DAY_IN_MS,
PREF_DEC_MASTER_KEY,
PREF_ICON_PACKS,
PREF_KEY_SECRETS, PREF_KEY_SETTINGS, PREF_MASTER_KEY } from '../common/conts';
PREF_KEY_SECRETS, PREF_KEY_SETTINGS, PREF_MASTER_KEY,
PREF_PASSWORD_CHALLENGE_TIME } from '../common/conts';
import { PREF_KEY_ITEMS } from '../common/conts';
import { AppLanguage, AuthTypePriority, DefaultUserPreferences, UserPreferences, ValueType } from '../common/settings';
import { AuthLevelSupport, generateMasterKeyInHUKS, genUserAuthType } from '../crypto/authUtils';
Expand All @@ -36,6 +38,7 @@ import {
EVENT_UPDATE_ITEM,
EVENT_UPDATE_ITEMS,
EVENT_UPDATE_PASSWORD,
EVENT_UPDATE_PASSWORD_CHALLENGE_TIME,
EVENT_UPDATE_SETTING,
FileExportRequestSchema,
FileImportRequestSchema,
Expand All @@ -47,6 +50,7 @@ import {
PasswordUpdateRequestSchema,
RemovalRequestSchema,
SettingUpdateRequestSchema,
UpdatePasswordChallengeTimeSchema,
UpdateRequestSchema } from '../common/events';
import { AigisImporter } from '../importers/aigis';
import { i18n } from '@kit.LocalizationKit';
Expand Down Expand Up @@ -502,6 +506,13 @@ export default class EntryAbility extends UIAbility {
AppStorage.setAndProp('EncMasterKey', encMasterKeyInfo);
AppStorage.setAndProp('MasterKeyAvailable', encMasterKeyInfo.keyAlias!==undefined);
AppStorage.setAndLink('StatusDecKeyRequired', encMasterKeyInfo.keyAlias!==undefined && decMasterKeyInfo.keyAlias===undefined);
// load last success password challenge time
let passwordChallengeTimestamp = pref.getSync(PREF_PASSWORD_CHALLENGE_TIME, 0) as number;
if (passwordChallengeTimestamp===0 || passwordChallengeTimestamp>Date.now()) {
passwordChallengeTimestamp = Date.now();
pref.putSync(PREF_PASSWORD_CHALLENGE_TIME, passwordChallengeTimestamp);
}
AppStorage.setAndProp(PREF_PASSWORD_CHALLENGE_TIME, passwordChallengeTimestamp);
// init controller
this.instPreferences = new PreferencesManager(
pref.getSync(PREF_KEY_ITEMS, []) as OTPItemInfo[],
Expand Down Expand Up @@ -673,12 +684,21 @@ export default class EntryAbility extends UIAbility {
if (this.dataPreferences && this.instPreferences) {
await this.instPreferences.updateMasterKeyInfo(data.password, data.newPassword, data.authLevel);
this.instPreferences.persists(this.dataPreferences);
// reset password challenge time
const _now = Date.now()
this.dataPreferences.putSync(PREF_PASSWORD_CHALLENGE_TIME, _now);
AppStorage.set(PREF_PASSWORD_CHALLENGE_TIME, _now);
// propagate
AppStorage.set('EncMasterKey', this.instPreferences.encMasterKeyInfo);
AppStorage.set('MasterKeyAvailable', true);
AppStorage.set('StatusDecKeyRequired', false);
}
});
this.context.eventHub.on(EVENT_UPDATE_PASSWORD_CHALLENGE_TIME, (data: UpdatePasswordChallengeTimeSchema) => {
const timestamp = data.timestamp;
this.dataPreferences?.putSync(PREF_PASSWORD_CHALLENGE_TIME, timestamp);
AppStorage.set(PREF_PASSWORD_CHALLENGE_TIME, timestamp);
});
this.context.eventHub.on(EVENT_EXPORT_REQUEST, async (data: FileExportRequestSchema) => {
if (!this.instPreferences!.MasterKeyAvailable || this.instPreferences===undefined) {
return;
Expand Down
36 changes: 32 additions & 4 deletions entry/src/main/ets/pages/Index.ets
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { LengthUnit, Offset, promptAction, SymbolGlyphModifier } from '@kit.ArkU
import { hilog } from '@kit.PerformanceAnalysisKit';
import { scanCore, scanBarcode } from '@kit.ScanKit';

import { FAKE_OTP_CODE, PREF_KEY_ITEMS } from '../common/conts';
import { FAKE_OTP_CODE, ONE_DAY_IN_MS, PREF_KEY_ITEMS, PREF_PASSWORD_CHALLENGE_TIME } from '../common/conts';
import { ScrollEventExtension, ScrollPosition } from '../common/eventExtension';
import { OTP, OTPSchema, parseURI, TimedOTPSchema } from '../crypto/otpUtils';
import { fuzzysearch } from '../common/utils';
Expand All @@ -24,10 +24,12 @@ import {
PasswordUpdateRequestSchema,
SettingUpdateRequestSchema,
UpdateRequestSchema } from '../common/events';
import { AppLockPage } from '../components/pages';
import { AppLockPage, PasswordChallengePage } from '../components/pages';
import { unifiedDataChannel } from '@kit.ArkData';
import { pasteboard } from '@kit.BasicServicesKit';
import { AuthTypePriority, DigitGroup, ItemSortOrder, ShowAccountName } from '../common/settings';
import { AuthTypePriority, DigitGroup, ItemSortOrder,
PasswordReminderPeriod,
ShowAccountName } from '../common/settings';
import { IconManager } from '../common/icons';
import { IssuerIcon } from '../components/icons';

Expand Down Expand Up @@ -107,6 +109,8 @@ class DragState<T> {
@Entry(storage)
@Component
struct Index {
@StorageProp(PREF_PASSWORD_CHALLENGE_TIME) PasswordChallengeTime: number = Date.now();
@StorageProp('settingsPasswordReminderPeriod') PasswordReminderPeriod: PasswordReminderPeriod = 'biweekly';
@StorageProp('EncMasterKey') EncMasterKey: MasterKeyInfo = {} as MasterKeyInfo;
@Watch('onDecKeyRequired') @StorageLink('StatusDecKeyRequired') StatusDecKeyRequired: boolean = false;
@Watch('onAuthLevelChanged') @StorageProp('settingsEnableBiometricUnlock') EnableBiometricUnlock: boolean = false;
Expand Down Expand Up @@ -134,6 +138,8 @@ struct Index {
// main content
if (this.EnableBiometricUnlock && !validateAtlAuth(this.atlAuth)) {
AppLockPage({atlAuth: this.atlAuth, authLevel:this.EnableBiometricAuth?'ATL3':'ATL1', priority: this.AuthTypePriority})
} else if (this.shouldShowPasswordChallenge()) {
PasswordChallengePage()
} else {
if (this.items.length===0) {
EmptyPage()
Expand Down Expand Up @@ -323,6 +329,28 @@ struct Index {
this.editState.hide();
}

private shouldShowPasswordChallenge(): boolean {
if (this.EncMasterKey.keyAlias===undefined) {
return false;
}
//
const delta_time = Date.now() - this.PasswordChallengeTime;
switch (this.PasswordReminderPeriod) {
case 'never':
return false;
case 'weekly':
return delta_time > 7*ONE_DAY_IN_MS;
case 'biweekly':
return delta_time > 14*ONE_DAY_IN_MS;
case 'monthly':
return delta_time > 30*ONE_DAY_IN_MS;
case 'quarterly':
return delta_time > 90*ONE_DAY_IN_MS;
default:
return false;
}
}

private getExKeys(): string[] {
if (this.editState.editNewItem) {
return this.items.map(x => x.keyAlias);
Expand Down Expand Up @@ -746,7 +774,7 @@ struct OTPItem {
.fontSize(24)
}
.height(40).width(40)
.backgroundColor($r('app.color.color_ribbon'))
.backgroundColor($r('sys.color.warning'))
.onClick((() => { this.requestMoveToTop(); }))
}
//
Expand Down
25 changes: 24 additions & 1 deletion entry/src/main/ets/pages/SettingsPage.ets
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { SettingListItemInfo, SettingListItem, SettingListItemToggle,
SettingListCollapsableItem,
NoInternetHyperLink,
SettingListIconItem} from "../components/settings";
import { AppLanguage, AuthTypePriority, DarkMode, DigitGroup, ShowAccountName, ValueType } from "../common/settings";
import { AppLanguage, AuthTypePriority, DarkMode, DigitGroup,
PasswordReminderPeriod,
ShowAccountName, ValueType } from "../common/settings";
import {
ChangePasswordInputDialog,
CustomSelectDialog, NewPasswordInputDialog, PasswordInputDialog } from "../components/dialog";
Expand Down Expand Up @@ -55,6 +57,14 @@ const AccountNameSelection: Map<ResourceStr, ShowAccountName> = new Map([
[$r('app.string.setting_show_account_name_option_off'), 'off'],
]);

const PasswordReminderSelection: Map<ResourceStr, PasswordReminderPeriod> = new Map([
[$r('app.string.setting_password_reminder_period_never'), 'never'],
[$r('app.string.setting_password_reminder_period_weekly'), 'weekly'],
[$r('app.string.setting_password_reminder_period_biweekly'), 'biweekly'],
[$r('app.string.setting_password_reminder_period_monthly'), 'monthly'],
// [$r('app.string.setting_password_reminder_period_quarterly'), 'quarterly'],
]);

const ATL1AuthTypePrioritySelection: Map<ResourceStr, AuthTypePriority> = new Map([
[$r('app.string.setting_auth_priority_face_first'), 'face'],
[$r('app.string.setting_auth_priority_touch_first'), 'touch'],
Expand Down Expand Up @@ -98,6 +108,8 @@ export struct SettingsPage {
@StorageProp('settingsShowIssuerIcons')
@Watch('onItChanged') ShowIssuerIcons: boolean = true;
// security
@StorageProp('settingsPasswordReminderPeriod')
@Watch('onItChanged') PasswordReminderPeriod: PasswordReminderPeriod = 'biweekly';
@StorageProp('settingsEnableBiometricUnlock')
@Watch('onItChanged') EnableBiometricUnlock: boolean = false;
@StorageProp('settingsAuthTypePriority')
Expand Down Expand Up @@ -262,6 +274,14 @@ export struct SettingsPage {
description: $r('app.string.setting_setup_password_hint'),
preview: this.MasterKeyAvailable ?$r('app.string.setting_password_confirmed') : undefined,
}).onClick(() => { this.handleSetupPassword() })
if (this.MasterKeyAvailable) {
SettingListItemSelect({
title: $r('app.string.setting_password_reminder_title'),
description: $r('app.string.setting_password_reminder_desc'),
selected: this.PasswordReminderPeriod,
entries: PasswordReminderSelection,
})
}
if (true) {
// if (!this.EnableBiometricAuth) { //FIXME: Not Support Cause of "External Error"
SettingListItemToggle({
Expand Down Expand Up @@ -414,6 +434,9 @@ export struct SettingsPage {
value = this.ShowIssuerIcons;
break;
// security
case 'PasswordReminderPeriod':
value = this.PasswordReminderPeriod;
break;
case 'EnableBiometricUnlock':
value = this.EnableBiometricUnlock;
break;
Expand Down
Loading

0 comments on commit 2f12e19

Please sign in to comment.