Skip to content

Commit

Permalink
Enhance types
Browse files Browse the repository at this point in the history
  • Loading branch information
mrruby committed Nov 19, 2023
1 parent aa11594 commit 8831f72
Show file tree
Hide file tree
Showing 23 changed files with 170 additions and 118 deletions.
2 changes: 1 addition & 1 deletion src/lib/components/Login.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
const isValid = await passwordExistStore.validate(password);
if (isValid) {
sessionStore.set({ session: true });
sessionStore.set(true);
location.reload();
} else {
alert('Invalid password');
Expand Down
2 changes: 2 additions & 0 deletions src/lib/const/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './secure-store';
export * from './query-keys';
1 change: 1 addition & 0 deletions src/lib/const/query-keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const GENERATED_KEYS = 'generatedKeys';
5 changes: 5 additions & 0 deletions src/lib/const/secure-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const SESSION = 'session';
export const LOCAL = 'local';

export const SESSION_DATA = 'sessionData';
export const PASSWORD_WITH_DEVICE_KEY = 'passwordWithDeviceKey';
88 changes: 51 additions & 37 deletions src/lib/helpers/encryption.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,77 @@
import type { SecureData } from '$types';

function hexStringToArrayBuffer(hexString: string) {
if (hexString.length % 2 !== 0) {
throw new Error('Invalid hexString length. It must be even.');
}
const bytes = new Uint8Array(hexString.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hexString.slice(i * 2, i * 2 + 2), 16);
}
return bytes.buffer;
}

function arrayBufferToBase64(buffer: ArrayBuffer): string {
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
}

function base64ToArrayBuffer(base64: string): ArrayBuffer {
const binary_string = atob(base64);
const len = binary_string.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}

function arrayBufferToHexString(buffer: ArrayBuffer) {
const byteArray = new Uint8Array(buffer);
let hexString = '';
byteArray.forEach(function (byte) {
hexString += ('0' + byte.toString(16)).slice(-2);
});
return hexString;
}

export async function hashPassword(password: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(password);
const algo: { name: string } = { name: 'SHA-256' };
const algo = { name: 'SHA-256' };
const hashBuffer = await crypto.subtle.digest(algo, data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join('');
return hashHex;
return arrayBufferToHexString(hashBuffer); // Convert ArrayBuffer to hex string
}

export async function encryptData(
secretData: Uint8Array,
passwordHash: string
passwordHashHex: string // Accept hex string
): Promise<SecureData> {
const iv = crypto.getRandomValues(new Uint8Array(12));
const algo: AesGcmParams = { name: 'AES-GCM', iv };
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(passwordHash),
algo,
false,
['encrypt']
);
const passwordHash = hexStringToArrayBuffer(passwordHashHex);
console.log('Imported key length (bytes):', passwordHash.byteLength);
const key = await crypto.subtle.importKey('raw', passwordHash, algo, false, ['encrypt']);

const encrypted = await crypto.subtle.encrypt(algo, key, secretData);
return {
encryptedData: new Uint8Array(encrypted),
iv
encryptedData: arrayBufferToBase64(encrypted),
iv: arrayBufferToBase64(iv)
};
}

export async function decryptData(
encryptedData: SecureData,
passwordHash: string
passwordHashHex: string // Accept hex string
): Promise<Uint8Array> {
const algo: AesGcmParams = { name: 'AES-GCM', iv: encryptedData.iv };
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(passwordHash),
const iv = base64ToArrayBuffer(encryptedData.iv);
const algo = { name: 'AES-GCM', iv };
const passwordHash = hexStringToArrayBuffer(passwordHashHex); // Convert hex string to ArrayBuffer
const key = await crypto.subtle.importKey('raw', passwordHash, algo, false, ['decrypt']);

const decrypted = await crypto.subtle.decrypt(
algo,
false,
['decrypt']
key,
base64ToArrayBuffer(encryptedData.encryptedData)
);

const decrypted = await crypto.subtle.decrypt(algo, key, encryptedData.encryptedData);
return new Uint8Array(decrypted);
}

export function arrayBufferToBase64(buffer: ArrayBuffer): string {
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
}

export function base64ToArrayBuffer(base64: string): ArrayBuffer {
const binary_string = atob(base64);
const len = binary_string.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}
1 change: 0 additions & 1 deletion src/lib/queries/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from './app-queries';
export * from './query-keys';
1 change: 0 additions & 1 deletion src/lib/queries/query-keys.ts

This file was deleted.

13 changes: 6 additions & 7 deletions src/lib/services/storage.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { isChromeStorageSafe } from '$helpers';
import type { AreaName, ChangesType, SecureData } from '$types';
import type { AreaName, ChangesType, PasswordAndSecureData } from '$types';
import type { LOCAL, SESSION, SESSION_DATA, PASSWORD_WITH_DEVICE_KEY } from '$const';

type StorageItem =
| { key: 'sessionData'; value: boolean; area: 'session' }
| { key: 'password'; value: string; area: 'local' }
| { key: 'encryptedDeviceKey'; value: SecureData; area: 'local' };
| { key: typeof SESSION_DATA; value: boolean; area: typeof SESSION }
| { key: typeof PASSWORD_WITH_DEVICE_KEY; value: PasswordAndSecureData; area: typeof LOCAL };

type GetStorageItem =
| { key: 'sessionData'; area: 'session' }
| { key: 'password'; area: 'local' }
| { key: 'encryptedDeviceKey'; area: 'local' };
| { key: typeof SESSION_DATA; area: typeof SESSION }
| { key: typeof PASSWORD_WITH_DEVICE_KEY; area: typeof LOCAL };

type StorageService = {
set: (item: StorageItem) => void;
Expand Down
1 change: 0 additions & 1 deletion src/lib/stores/const.ts

This file was deleted.

2 changes: 0 additions & 2 deletions src/lib/stores/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
export * from './passphrase';
export * from './password-exist';
export * from './password';
export * from './const';
export * from './keys-store';
export * from './session';
31 changes: 19 additions & 12 deletions src/lib/stores/password-exist.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import { z } from 'zod';
import { writable } from 'svelte/store';
import { storageService } from '$services';
import type { AreaName, ChangesType } from '$types';
import { PasswordAndSecureDataSchema, type AreaName, type ChangesType } from '$types';
import { hashPassword } from '$helpers';
import { LOCAL, PASSWORD_WITH_DEVICE_KEY } from '$const';

const createPasswordExistStore = () => {
const { subscribe, set, update } = writable<boolean | null>(null, () => {
const { subscribe, update } = writable<boolean | null>(null, () => {
const validateAndUpdate = (result: unknown) => {
const validatedResult = PasswordAndSecureDataSchema.safeParse(result);
update(() => validatedResult.success);
};

const listener = (changes: ChangesType, namespace: AreaName) => {
if (namespace === 'local' && 'password' in changes) {
update(() => true);
if (namespace === LOCAL) {
validateAndUpdate(changes[PASSWORD_WITH_DEVICE_KEY]);
}
};

storageService.get(
{
key: 'password',
area: 'local'
key: PASSWORD_WITH_DEVICE_KEY,
area: LOCAL
},
(result) => set(!!result)
(result: unknown) => validateAndUpdate(result)
);

storageService.addListener(listener);
Expand All @@ -31,12 +36,14 @@ const createPasswordExistStore = () => {
subscribe,
validate: async (password: string) => {
const result = await storageService.getWithoutCallback({
key: 'password',
area: 'local'
key: PASSWORD_WITH_DEVICE_KEY,
area: LOCAL
});
const validatedResult = z.string().safeParse(result);
const validatedResult = PasswordAndSecureDataSchema.safeParse(result);

return validatedResult.success && (await hashPassword(password)) === validatedResult.data;
return (
validatedResult.success && (await hashPassword(password)) === validatedResult.data.password
);
}
};
};
Expand Down
14 changes: 0 additions & 14 deletions src/lib/stores/password.ts

This file was deleted.

28 changes: 13 additions & 15 deletions src/lib/stores/session.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,27 @@
import { writable } from 'svelte/store';
import { type SessionState, SessionStateSchema, type AreaName, type ChangesType } from '$types';
import { storageService } from '$services';
import { SESSION, SESSION_DATA } from '$const';

const createSessionStore = () => {
const { subscribe, set, update } = writable<SessionState>(null, () => {
const validateAndUpdate = (result: unknown) => {
const validatedResult = SessionStateSchema.safeParse(result);
update(() => (validatedResult.success ? validatedResult.data : false));
};

const listener = (changes: ChangesType, namespace: AreaName) => {
if (namespace === 'session') {
const newValue = SessionStateSchema.safeParse(changes['sessionData']);
update(() => (newValue.success ? newValue.data : false));
if (namespace === SESSION) {
validateAndUpdate(changes[SESSION_DATA]);
}
};

storageService.get(
{
key: 'sessionData',
area: 'session'
key: SESSION_DATA,
area: SESSION
},
(result: unknown) => {
const validatedResult = SessionStateSchema.safeParse(result);
if (validatedResult.success) {
set(validatedResult.data);
} else {
set(false);
}
}
(result: unknown) => validateAndUpdate(result)
);

storageService.addListener(listener);
Expand All @@ -38,9 +36,9 @@ const createSessionStore = () => {
set: (value: SessionState) => {
set(value);
storageService.set({
key: 'sessionData',
key: SESSION_DATA,
value: value ?? false,
area: 'session'
area: SESSION
});
}
};
Expand Down
13 changes: 11 additions & 2 deletions src/lib/types/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,18 @@ export type KeysState = {
loading: boolean;
};

export const Password = z.string();

export const SecureDataSchema = z.object({
encryptedData: z.instanceof(Uint8Array),
iv: z.instanceof(Uint8Array)
encryptedData: z.string(),
iv: z.string()
});

export type SecureData = z.infer<typeof SecureDataSchema>;

export const PasswordAndSecureDataSchema = z.object({
password: Password,
secureData: SecureDataSchema
});

export type PasswordAndSecureData = z.infer<typeof PasswordAndSecureDataSchema>;
3 changes: 2 additions & 1 deletion src/lib/types/storage-service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { LOCAL, SESSION } from '$const';
import { z } from 'zod';

export type AreaName = 'session' | 'local' | 'sync' | 'managed';
export type AreaName = typeof SESSION | typeof LOCAL | 'sync' | 'managed';

export const SessionStateSchema = z.boolean().nullable();

Expand Down
2 changes: 1 addition & 1 deletion src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<script>
<script lang="ts">
import '../app.css';
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
Expand Down
25 changes: 22 additions & 3 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
<script>
<script lang="ts">
import { onMount } from 'svelte';
import { Init, Login } from '$components';
import { sessionStore, passwordExistStore } from '$stores';
import { derived } from 'svelte/store';
import { storageService } from '$services';
import { LOCAL, PASSWORD_WITH_DEVICE_KEY, SESSION, SESSION_DATA } from '$const';
let loading = true;
onMount(() => {
storageService.get(
{
key: SESSION_DATA,
area: SESSION
},
(result: unknown) => console.log(result)
);
storageService.get(
{
key: PASSWORD_WITH_DEVICE_KEY,
area: LOCAL
},
(result: unknown) => console.log(result)
);
const storeValue = derived(
[sessionStore, passwordExistStore],
([$sessionStore, $passwordExistStore]) =>
$sessionStore === null || $passwordExistStore === null
([$sessionStore, $passwordExistStore]) => {
console.log($sessionStore, $passwordExistStore);
return $sessionStore === null || $passwordExistStore === null;
}
);
storeValue.subscribe(($loading) => {
loading = $loading;
Expand Down
5 changes: 1 addition & 4 deletions src/routes/setup/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
<script>
import { page } from '$app/stores';
import { onMount, setContext } from 'svelte';
import { passphrase, passphraseStore } from '$stores';
import { onMount } from 'svelte';
import { createIsAuthenticated, dismissWindow } from '$helpers';
const goBack = () => window.history.back();
setContext(passphrase, passphraseStore);
$: allowGoBack = !(
$page.url.pathname.includes('start') || $page.url.pathname.includes('download')
);
Expand Down
Loading

0 comments on commit 8831f72

Please sign in to comment.