Skip to content

Commit

Permalink
Lock screen can work
Browse files Browse the repository at this point in the history
  • Loading branch information
asika32764 committed Mar 24, 2024
1 parent d069f33 commit a52ae6a
Show file tree
Hide file tree
Showing 17 changed files with 398 additions and 25 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"vue-router": "^4.2.0"
},
"devDependencies": {
"@aparajita/capacitor-biometric-auth": "^7.1.1",
"@asika32764/vite-plugin-show-env": "^1.0.3",
"@asika32764/vue-animate": "^3.0.2",
"@capacitor-community/barcode-scanner": "^4.0.1",
Expand Down
13 changes: 11 additions & 2 deletions src/components/StoreViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ import {
encSecretStorage,
isLogin,
kekStorage, mainStore,
refreshTokenStorage,
refreshTokenStorage, saltStorage,
userStorage,
} from '@/store/main-store';
import { enableBiometricsOption } from '@/store/options-store';
import { reactive } from 'vue';
const options = reactive({
enableBiometricsOption,
});
function store(...args: any[]) {
return 'store-viewer';
Expand All @@ -23,9 +29,12 @@ function store(...args: any[]) {
encMasterStorage,
encSecretStorage,
isLogin,
kekStorage, mainStore,
kekStorage,
saltStorage,
mainStore,
refreshTokenStorage,
userStorage,
options,
)"></div>
</template>

Expand Down
3 changes: 2 additions & 1 deletion src/components/layout/MainLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ withDefaults(
title?: string;
headerCondense?: boolean;
showMenuButton?: boolean;
color?: string;
}>(),
{
headerCondense: false,
Expand Down Expand Up @@ -48,7 +49,7 @@ withDefaults(
</ion-toolbar>
</ion-header>

<ion-content>
<ion-content :color fullscreen>
<HeaderCondense v-if="title && headerCondense">
{{ title }}
</HeaderCondense>
Expand Down
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,7 @@ router.isReady().then(async () => {

if (!isLogin.value) {
router.replace('/auth/login');
} else {
// router.replace('/lock');
}
});
5 changes: 5 additions & 0 deletions src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ const routes: Array<RouteRecordRaw> = [
name: 'registration',
component: () => import('@/views/auth/RegistrationPage.vue')
},
{
path: '/lock',
name: 'lock',
component: () => import('@/views/LockScreen.vue')
},
{
path: '/pages/',
component: () => import('@/views/MainPage.vue'),
Expand Down
13 changes: 12 additions & 1 deletion src/service/auth-service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import apiClient from '@/service/api-client';
import { sodiumCipher } from '@/service/cipher';
import encryptionService from '@/service/encryption-service';
import { encMasterStorage, encSecretStorage, kekStorage } from '@/store/main-store';
import {
accessTokenStorage,
encMasterStorage,
encSecretStorage,
kekStorage, refreshTokenStorage,
saltStorage,
userStorage,
} from '@/store/main-store';
import { User } from '@/types';
import {
base64UrlDecode,
Expand Down Expand Up @@ -51,8 +58,12 @@ export default new class AuthService {

const S = data.S.toString();

saltStorage.value = hexToBigint(salt).toString();
encSecretStorage.value = uint82text(await sodiumCipher.decrypt(base64UrlDecode(data.encSecret), S));
encMasterStorage.value = uint82text(await sodiumCipher.decrypt(base64UrlDecode(data.encMaster), S));
userStorage.value = data.user;
accessTokenStorage.value = data.accessToken;
refreshTokenStorage.value = data.refreshToken;

return data;
}
Expand Down
39 changes: 39 additions & 0 deletions src/service/idle-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import lockScreenService from '@/service/lock-screen-service';
import { userStorage } from '@/store/main-store';

export default new class {
lastActionTime = 0;

listen(maxIdleMinutes = 5, intervalSecs = 60) {
setInterval(() => {
this.checkIdleTimeAndHandle(maxIdleMinutes);
}, intervalSecs * 1000);

this.logLastActionTime();

document.body.addEventListener('click', () => {
this.logLastActionTime();
});
}

logLastActionTime() {
this.lastActionTime = Math.round(new Date().getTime() / 1000);
}

async checkIdleTimeAndHandle(maxIdleMinutes: number) {
const now = Math.round(new Date().getTime() / 1000);
const maxIdleSeconds = maxIdleMinutes * 60;
const idleTime = now - this.lastActionTime;

// console.log('idle time', idleTime, '>', maxIdleSeconds, '=', idleTime > maxIdleSeconds);

if (!userStorage.value) {
return;
}

if (idleTime > maxIdleSeconds) {
lockScreenService.lock();
}
}
}

74 changes: 74 additions & 0 deletions src/service/lock-screen-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import router from '@/router';
import encryptionService from '@/service/encryption-service';
import { isLock, isManuallyLock, kekStorage, saltStorage } from '@/store/main-store';
import { enableBiometricsOption } from '@/store/options-store';
import { simpleAlert } from '@/utilities/alert';
import secretToolkit, { Encoder } from '@/utilities/secret-toolkit';
import {
AndroidBiometryStrength,
BiometricAuth,
BiometryType,
} from '@aparajita/capacitor-biometric-auth';
import { Capacitor } from '@capacitor/core';
import { timingSafeEquals } from '@windwalker-io/srp';

export default new class {
async lock() {
isLock.value = true;

router.replace({ name: 'lock' });
}

async unlock() {
isLock.value = false;
isManuallyLock.value = false;
}

async passwordAuthenticate(password: string) {
const kek = secretToolkit.encode(
await encryptionService.deriveKek(password, saltStorage.value),
Encoder.HEX
);

if (!timingSafeEquals(kekStorage.value, kek)) {
throw new Error('Invalid password');
}
}

async testBiometrics() {
try {
await this.biometricsAuthenticate();
} catch (e) {
if (e instanceof Error) {
simpleAlert(e.message);
}
}
}

async biometricsAuthenticate() {
if (!Capacitor.isNativePlatform()) {
// web simulate
await BiometricAuth.setBiometryType(BiometryType.touchId);
await BiometricAuth.setBiometryIsEnrolled(true);
await BiometricAuth.setDeviceIsSecure(true);
}

const info = await BiometricAuth.checkBiometry();

if (!info.isAvailable) {
throw new Error('Touch ID or Face ID not available.');
}

await BiometricAuth.authenticate({
reason: 'Please authenticate',
cancelTitle: 'Cancel',
allowDeviceCredential: true,
iosFallbackTitle: 'Use password',
androidTitle: 'Biometric login',
androidSubtitle: 'Log in using biometric authentication',
androidConfirmationRequired: true,
androidBiometryStrength: AndroidBiometryStrength.weak,
});
}
}

7 changes: 6 additions & 1 deletion src/store/main-store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Account, User } from '@/types';
import { StorageSerializers, useLocalStorage } from '@vueuse/core';
import { computed, reactive } from 'vue';
import { computed, reactive, ref } from 'vue';

export const mainStore = reactive<{
user?: User;
Expand All @@ -21,6 +21,7 @@ export const userStorage = useLocalStorage<User | null>(
);
export const isLogin = computed(() => accessTokenStorage.value !== '');

export const saltStorage = useLocalStorage('@authman:salt', '');
export const encSecretStorage = useLocalStorage('@authman:enc.secret', '');
export const encMasterStorage = useLocalStorage('@authman:enc.master', '');
export const kekStorage = useLocalStorage('@authman:kek', '');
Expand All @@ -29,3 +30,7 @@ export const accountsLoaded = useLocalStorage('@authman:accounts.loaded', false,
serializer: StorageSerializers.boolean
});
export const accountsStorage = useLocalStorage<Account<string>[]>('@authman:accounts', []);

// Lock
export const isLock = ref(true);
export const isManuallyLock = ref(false);
9 changes: 9 additions & 0 deletions src/store/options-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { StorageSerializers, useLocalStorage } from '@vueuse/core';

export const enableBiometricsOption = useLocalStorage(
'@authman:config:enable.biometrics',
true,
{
serializer: StorageSerializers.boolean
}
);
1 change: 1 addition & 0 deletions src/types/entity/user.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export interface User {
id: string;
email: string;
name: string;
lastReset: string;
params: any;
[name: string]: any;
}
6 changes: 4 additions & 2 deletions src/utilities/loading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import { ref, Ref } from 'vue';
export default function useLoading(loading?: Ref<boolean>) {
loading = loading || ref(false);

const run = async function <T>(callback: () => Promise<T>): Promise<T> {
const run = async function <T>(callback: () => Promise<T>, errorAlert = true): Promise<T> {
loading!.value = true;

try {
return await callback();
} catch (e: any) {
console.error(e);
simpleAlert(e?.message || 'Unknown Error', '');
if (errorAlert) {
simpleAlert(e?.message || 'Unknown Error', '');
}
throw e;
} finally {
loading!.value = false;
Expand Down
Loading

0 comments on commit a52ae6a

Please sign in to comment.