Skip to content

Commit

Permalink
fix(dw): session
Browse files Browse the repository at this point in the history
  • Loading branch information
javadkh2 committed Dec 12, 2024
1 parent 70ce0b6 commit 1c02820
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 41 deletions.
28 changes: 20 additions & 8 deletions packages/apps/dev-wallet/src/modules/security/fallback.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,25 @@ export interface SecureContext {
ttl?: number;
}

const getPassword = (sessionEntropy: string, encryptionKey: Uint8Array) => {
const phrase = new TextEncoder().encode(sessionEntropy);
const password = new Uint8Array(phrase.length + encryptionKey.length);
password.set(phrase);
password.set(encryptionKey, phrase.length);
return password;
};

export function fallbackSecurityService() {
let context: SecureContext | null = null;
let clearTimer: NodeJS.Timeout | null = null;
console.log('Service Worker is not available, using fallback service');

async function setSecurityPhrase({
sessionEntropy,
phrase,
keepPolicy,
ttl,
}: {
phrase: string;
keepPolicy: ISetSecurityPhrase['payload']['keepPolicy'];
ttl?: number;
}) {
}: ISetSecurityPhrase['payload']) {
if (clearTimer) {
clearTimeout(clearTimer);
clearTimer = null;
Expand All @@ -32,7 +37,11 @@ export function fallbackSecurityService() {
const encryptionKey = randomBytes(32);
context = {
encryptionKey,
encryptionPhrase: await kadenaEncrypt(encryptionKey, phrase, 'buffer'),
encryptionPhrase: await kadenaEncrypt(
getPassword(sessionEntropy, encryptionKey),
phrase,
'buffer',
),
keepPolicy: keepPolicy,
};
if (context.keepPolicy === 'short-time') {
Expand All @@ -46,12 +55,15 @@ export function fallbackSecurityService() {
return { result: 'success' };
}

async function getSecurityPhrase() {
async function getSecurityPhrase(sessionEntropy: string) {
if (!context) {
return null;
}
return new TextDecoder().decode(
await kadenaDecrypt(context.encryptionKey, context.encryptionPhrase),
await kadenaDecrypt(
getPassword(sessionEntropy, context.encryptionKey),
context.encryptionPhrase,
),
);
}

Expand Down
15 changes: 4 additions & 11 deletions packages/apps/dev-wallet/src/modules/security/security.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,19 @@ import {
import { sendMessageToServiceWorker } from '@/utils/service-worker-com';
import { fallbackSecurityService } from './fallback.service';

async function setSecurityPhrase({
phrase,
keepPolicy,
ttl,
}: {
phrase: string;
keepPolicy: ISetSecurityPhrase['payload']['keepPolicy'];
ttl?: number;
}) {
async function setSecurityPhrase(payload: ISetSecurityPhrase['payload']) {
const { result } = (await sendMessageToServiceWorker({
action: 'setSecurityPhrase',
payload: { phrase, keepPolicy, ttl },
payload,
})) as ISetPhraseResponse;

return { result };
}

async function getSecurityPhrase() {
async function getSecurityPhrase(sessionEntropy: string) {
const { phrase } = (await sendMessageToServiceWorker({
action: 'getSecurityPhrase',
payload: { sessionEntropy },
})) as IGetPhraseResponse;
return phrase;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ function usePassword(profile: IProfile | undefined) {
profileRef.current = profile;
const prompt = usePrompt();
const getPassword = useCallback(async () => {
const phrase = await securityService.getSecurityPhrase();
debugger;

Check failure on line 78 in packages/apps/dev-wallet/src/modules/wallet/wallet.provider.tsx

View workflow job for this annotation

GitHub Actions / Build & unit test

Unexpected 'debugger' statement
const phrase = await securityService.getSecurityPhrase(
Session.get('sessionId') as string,
);
return phrase;
}, []);

Expand All @@ -87,6 +90,7 @@ function usePassword(profile: IProfile | undefined) {
const { result } = (await securityService.setSecurityPhrase({
phrase: password,
keepPolicy,
sessionEntropy: Session.get('sessionId') as string,
})) as ISetPhraseResponse;
if (result !== 'success') {
throw new Error('Failed to set password');
Expand Down
6 changes: 6 additions & 0 deletions packages/apps/dev-wallet/src/service-worker/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,9 @@ export async function kadenaDecrypt(

throw new Error('Decryption failed');
}

export const unit8arrayToString = (unit8array: Uint8Array) =>
new TextDecoder().decode(unit8array);

export const stringToUint8Array = (str: string) =>
new TextEncoder().encode(str);
102 changes: 86 additions & 16 deletions packages/apps/dev-wallet/src/service-worker/service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { SecureContext, ServiceWorkerMessage } from './types';

declare const self: ServiceWorkerGlobalScope;

let context: SecureContext | null = null;
let clearTimer: NodeJS.Timeout | null = null;

const version = 'v1';
Expand All @@ -19,12 +18,72 @@ self.addEventListener('activate', () => {
self.clients.claim(); // Take control of open clients (pages)
});

const setContext = async (context: SecureContext) => {
caches.open('sec-context').then((cache) => {
const buffer = new Uint8Array(
4 +
[
context.encryptionKey.length,
...context.encryptionPhrase.map((b) => b.length),
].reduce((acc, cur) => acc + cur, 0),
);
buffer[0] = context.encryptionKey.length;
buffer[1] = context.encryptionPhrase[0].length;
buffer[2] = context.encryptionPhrase[1].length;
buffer[3] = context.encryptionPhrase[2].length;
let offset = 4;
[context.encryptionKey, ...context.encryptionPhrase].forEach((phrase) => {
buffer.set(phrase, offset);
offset += phrase.length;
});
cache.put(
'context',
new Response(buffer, {
headers: { 'Content-Type': 'application/octet-stream' },
}),
);
});
};

const getContext = async () => {
const cache = await caches.open('sec-context');
const response = await cache.match('context');
if (!response) return null;
const buffer = await response.arrayBuffer();
const view = new Uint8Array(buffer);
const encryptionKey = view.subarray(4, 4 + view[0]);
const encryptionPhrase = [
view.subarray(4 + view[0], 4 + view[0] + view[1]),
view.subarray(4 + view[0] + view[1], 4 + view[0] + view[1] + view[2]),
view.subarray(
4 + view[0] + view[1] + view[2],
4 + view[0] + view[1] + view[2] + view[3],
),
] as [Uint8Array, Uint8Array, Uint8Array];
return {
encryptionKey,
encryptionPhrase,
};
};

const clearContext = async () => {
const cache = await caches.open('sec-context');
await cache.delete('context');
};

const getPassword = (sessionEntropy: string, encryptionKey: Uint8Array) => {
const phrase = new TextEncoder().encode(sessionEntropy);
const password = new Uint8Array(phrase.length + encryptionKey.length);
password.set(phrase);
password.set(encryptionKey, phrase.length);
return password;
};

self.addEventListener('message', async (event) => {
const { action } = event.data as ServiceWorkerMessage;
const { action, payload } = event.data as ServiceWorkerMessage;

switch (action) {
case 'setSecurityPhrase': {
const { payload } = event.data;
if (clearTimer) {
clearTimeout(clearTimer);
clearTimer = null;
Expand All @@ -33,25 +92,32 @@ self.addEventListener('message', async (event) => {
event.ports[0].postMessage({ result: 'success' });
return;
}
const sessionEntropy = payload.sessionEntropy;
if (!sessionEntropy) {
throw new Error('Session entropy is required');
}
const encryptionKey = randomBytes(32);
context = {
setContext({
encryptionKey,
encryptionPhrase: await kadenaEncrypt(encryptionKey, payload.phrase),
encryptionPhrase: await kadenaEncrypt(
getPassword(sessionEntropy, encryptionKey),
payload.phrase,
),
keepPolicy: payload.keepPolicy,
};
if (context.keepPolicy === 'short-time') {
clearTimer = setTimeout(
() => {
context = null;
},
payload.ttl || 5 * 60 * 1000,
);
});
if (payload.keepPolicy === 'short-time') {
clearTimer = setTimeout(clearContext, payload.ttl || 5 * 60 * 1000);
}
event.ports[0].postMessage({ result: 'success' });
break;
}

case 'getSecurityPhrase':
case 'getSecurityPhrase': {
const sessionEntropy: string = payload.sessionEntropy;
if (!sessionEntropy) {
throw new Error('Session entropy is required');
}
const context = await getContext();
if (!context) {
event.ports[0].postMessage({
phrase: null,
Expand All @@ -60,13 +126,17 @@ self.addEventListener('message', async (event) => {
}
event.ports[0].postMessage({
phrase: new TextDecoder().decode(
await kadenaDecrypt(context.encryptionKey, context.encryptionPhrase),
await kadenaDecrypt(
getPassword(sessionEntropy, context.encryptionKey),
context.encryptionPhrase,
),
),
});
break;
}

case 'clearSecurityPhrase': {
context = null;
clearContext();
event.ports[0].postMessage({ result: 'success' });
break;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/apps/dev-wallet/src/service-worker/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export interface ISetSecurityPhrase {
action: 'setSecurityPhrase';
payload: {
sessionEntropy: string;
keepPolicy: 'session' | 'short-time' | 'never';
phrase: string;
ttl?: number;
Expand All @@ -9,10 +10,14 @@ export interface ISetSecurityPhrase {

export interface IGetSecurityPhrase {
action: 'getSecurityPhrase';
payload: {
sessionEntropy: string;
};
}

export interface IClearSecurityPhrase {
action: 'clearSecurityPhrase';
payload?: undefined;
}

export interface ISetPhraseResponse {
Expand Down
15 changes: 10 additions & 5 deletions packages/apps/dev-wallet/src/utils/session.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { config } from '@/config';
import { UUID } from '@/modules/types';
import { getErrorMessage } from './getErrorMessage';
import { createEventEmitter, throttle } from './helpers';

type SessionValue = { expiration?: string; creationDate?: string } & Record<
string,
unknown
>;
type SessionValue = {
expiration?: string;
creationDate?: string;
sessionId?: UUID;
} & Record<string, unknown>;

const serialization = {
serialize: async (session: SessionValue) => JSON.stringify(session),
Expand All @@ -19,6 +21,7 @@ export function createSession(key: string = 'session') {
let session: SessionValue = {
creationDate: `${Date.now()}`,
expiration: `${Date.now() + config.SESSION.TTL}`,
sessionId: crypto.randomUUID(),
};

const eventEmitter = createEventEmitter<{
Expand Down Expand Up @@ -93,6 +96,7 @@ export function createSession(key: string = 'session') {
session = {
creationDate: `${Date.now()}`,
expiration: `${Date.now() + config.SESSION.TTL}`,
sessionId: crypto.randomUUID(),
};
}
}
Expand All @@ -106,7 +110,7 @@ export function createSession(key: string = 'session') {
session[key] = value;
await renew();
},
get: (key: string) => session[key],
get: (key: keyof SessionValue) => session[key],
clear: () => {
localStorage.removeItem('session');
session = {};
Expand All @@ -116,6 +120,7 @@ export function createSession(key: string = 'session') {
reset: () => {
session = {
creationDate: `${Date.now()}`,
sessionId: crypto.randomUUID(),
};
return renew();
},
Expand Down

0 comments on commit 1c02820

Please sign in to comment.