Skip to content

Commit

Permalink
Fix data
Browse files Browse the repository at this point in the history
  • Loading branch information
mrruby committed Nov 17, 2023
1 parent bc967fe commit aa11594
Show file tree
Hide file tree
Showing 12 changed files with 138 additions and 45 deletions.
2 changes: 1 addition & 1 deletion src/lib/helpers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import { derived, type Readable } from 'svelte/store';

export function createIsAuthenticated(): Readable<boolean> {
return derived([sessionStore, passwordExistStore], ([$sessionStore, $passwordExistStore]) =>
Boolean($sessionStore.session && $passwordExistStore)
Boolean($sessionStore && $passwordExistStore)
);
}
63 changes: 63 additions & 0 deletions src/lib/helpers/encryption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { SecureData } from '$types';

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 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;
}

export async function encryptData(
secretData: Uint8Array,
passwordHash: 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 encrypted = await crypto.subtle.encrypt(algo, key, secretData);
return {
encryptedData: new Uint8Array(encrypted),
iv
};
}

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

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;
}
2 changes: 1 addition & 1 deletion src/lib/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './navigation';
export * from './other';
export * from './password';
export * from './encryption';
export * from './auth';
4 changes: 3 additions & 1 deletion src/lib/helpers/other.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export const isChromeDefined = () => typeof chrome !== 'undefined';

export const isChromeStorageSafe = () =>
typeof chrome !== 'undefined' && chrome.storage && chrome.storage.session;
isChromeDefined() && chrome.storage && chrome.storage.session;
8 changes: 0 additions & 8 deletions src/lib/helpers/password.ts

This file was deleted.

28 changes: 19 additions & 9 deletions src/lib/services/storage.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
import { isChromeStorageSafe } from '$helpers';
import type { AreaName, ChangesType, StorageKey } from '$types';
import type { AreaName, ChangesType, SecureData } from '$types';

type StorageItem =
| { key: 'sessionData'; value: boolean; area: 'session' }
| { key: 'password'; value: string; area: 'local' }
| { key: 'encryptedDeviceKey'; value: SecureData; area: 'local' };

type GetStorageItem =
| { key: 'sessionData'; area: 'session' }
| { key: 'password'; area: 'local' }
| { key: 'encryptedDeviceKey'; area: 'local' };

type StorageService = {
set(key: StorageKey, value: unknown, area: 'local' | 'session'): void;
get(key: StorageKey, callback: (value: unknown) => void, area: 'local' | 'session'): void;
getWithoutCallback: (key: StorageKey, area: 'local' | 'session') => Promise<unknown>;
addListener(listener: (changes: ChangesType, namespace: AreaName) => void): void;
removeListener(listener: (changes: ChangesType, namespace: AreaName) => void): void;
set: (item: StorageItem) => void;
get: (item: GetStorageItem, callback: (value: unknown) => void) => void;
getWithoutCallback: (item: GetStorageItem) => Promise<unknown>;
addListener: (listener: (changes: ChangesType, namespace: AreaName) => void) => void;
removeListener: (listener: (changes: ChangesType, namespace: AreaName) => void) => void;
};

export const storageService: StorageService = {
set: (key: StorageKey, value: unknown, area: 'local' | 'session') => {
set: ({ key, value, area }) => {
if (isChromeStorageSafe()) {
chrome.storage[area].set({ [key]: value });
}
},
get: (key: StorageKey, callback: (value: unknown) => void, area: 'local' | 'session') => {
get: ({ key, area }, callback) => {
if (isChromeStorageSafe()) {
chrome.storage[area].get([key], (result: ChangesType) => {
callback(result[key]);
Expand All @@ -24,7 +34,7 @@ export const storageService: StorageService = {
callback(null);
}
},
getWithoutCallback: (key: StorageKey, area: 'local' | 'session') => {
getWithoutCallback: ({ key, area }) => {
if (isChromeStorageSafe()) {
return new Promise((resolve) => {
chrome.storage[area].get([key], (result: ChangesType) => {
Expand Down
17 changes: 11 additions & 6 deletions src/lib/stores/password-exist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ const createPasswordExistStore = () => {
}
};

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

storageService.addListener(listener);

Expand All @@ -23,12 +29,11 @@ const createPasswordExistStore = () => {

return {
subscribe,
set: (value: boolean) => {
set(value);
storageService.set('password', value, 'local');
},
validate: async (password: string) => {
const result = await storageService.getWithoutCallback('password', 'local');
const result = await storageService.getWithoutCallback({
key: 'password',
area: 'local'
});
const validatedResult = z.string().safeParse(result);

return validatedResult.success && (await hashPassword(password)) === validatedResult.data;
Expand Down
22 changes: 14 additions & 8 deletions src/lib/stores/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,27 @@ import { type SessionState, SessionStateSchema, type AreaName, type ChangesType
import { storageService } from '$services';

const createSessionStore = () => {
const { subscribe, set, update } = writable<SessionState>({ session: null }, () => {
const { subscribe, set, update } = writable<SessionState>(null, () => {
const listener = (changes: ChangesType, namespace: AreaName) => {
if (namespace === 'session') {
const newValue = SessionStateSchema.safeParse(changes['sessionData']);
update(() => (newValue.success ? newValue.data : { session: false }));
update(() => (newValue.success ? newValue.data : false));
}
};

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

storageService.addListener(listener);
Expand All @@ -35,7 +37,11 @@ const createSessionStore = () => {
subscribe,
set: (value: SessionState) => {
set(value);
storageService.set('sessionData', value, 'session');
storageService.set({
key: 'sessionData',
value: value ?? false,
area: 'session'
});
}
};
};
Expand Down
9 changes: 9 additions & 0 deletions src/lib/types/keys.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { z } from 'zod';

export type SetSecret = 'set' | 'confirm';

export type GeneratedKeys = {
Expand All @@ -10,3 +12,10 @@ export type KeysState = {
keys: GeneratedKeys;
loading: boolean;
};

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

export type SecureData = z.infer<typeof SecureDataSchema>;
6 changes: 1 addition & 5 deletions src/lib/types/storage-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@ import { z } from 'zod';

export type AreaName = 'session' | 'local' | 'sync' | 'managed';

export const SessionStateSchema = z.object({
session: z.boolean().nullable()
});
export const SessionStateSchema = z.boolean().nullable();

export type SessionState = z.infer<typeof SessionStateSchema>;

export type StorageKey = 'sessionData' | 'password' | 'encryptedDeviceKey';

export type ChangesType = {
[key: string]: unknown;
};
4 changes: 2 additions & 2 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
const storeValue = derived(
[sessionStore, passwordExistStore],
([$sessionStore, $passwordExistStore]) =>
$sessionStore.session === null || $passwordExistStore === null
$sessionStore === null || $passwordExistStore === null
);
storeValue.subscribe(($loading) => {
loading = $loading;
Expand All @@ -20,7 +20,7 @@

{#if loading}
<span>Loading</span>
{:else if $sessionStore.session}
{:else if $sessionStore}
<span>Session</span>
{:else if $passwordExistStore}
<Login />
Expand Down
18 changes: 14 additions & 4 deletions src/routes/setup/app-password/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { goto } from '$app/navigation';
import { EnterSecretComponent } from '$components';
import { onMount } from 'svelte';
import { hashPassword } from '$helpers';
import { encryptData, hashPassword } from '$helpers';
import { storageService } from '$services';
onMount(() => {
Expand All @@ -14,9 +14,19 @@
}
});
async function setPassword(password: string): Promise<void> {
storageService.set('password', await hashPassword(password), 'local');
}
const setPassword = async (password: string): Promise<void> => {
if ($keysStore.keys.device !== null) {
const hash = await hashPassword(password);
storageService.set({ key: 'password', value: hash, area: 'local' });
storageService.set({
key: 'encryptedDeviceKey',
value: await encryptData($keysStore.keys.device, password),
area: 'local'
});
window.close();
}
};
let appPasswordState: SetSecret = 'set';
let confirmPassword = '';
Expand Down

0 comments on commit aa11594

Please sign in to comment.