Some checks failed
Podman Rootless Demo / test-backend (push) Has been skipped
Podman Rootless Demo / test-frontend (push) Has been skipped
Podman Rootless Demo / build-backend (push) Failing after 1s
Podman Rootless Demo / deploy-prod (push) Has been skipped
Podman Rootless Demo / build-frontend (push) Has been skipped
212 lines
No EOL
5.5 KiB
TypeScript
212 lines
No EOL
5.5 KiB
TypeScript
/**
|
|
* Secure storage for encrypted passport data
|
|
*/
|
|
|
|
import type { UserProfile } from './types';
|
|
|
|
// Storage keys
|
|
const STORAGE_KEYS = {
|
|
ENCRYPTED_PASSPORT: 'sharenet_encrypted_passport',
|
|
STORAGE_PREFERENCE: 'sharenet_storage_preference',
|
|
SESSION_KEY: 'sharenet_session_key',
|
|
} as const;
|
|
|
|
type StoragePreference = 'session' | 'persistent';
|
|
|
|
/**
|
|
* Generate a random encryption key using Web Crypto API
|
|
*/
|
|
async function generateEncryptionKey(): Promise<CryptoKey> {
|
|
return await window.crypto.subtle.generateKey(
|
|
{
|
|
name: 'AES-GCM',
|
|
length: 256,
|
|
},
|
|
true, // extractable
|
|
['encrypt', 'decrypt']
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Encrypt data using AES-GCM
|
|
*/
|
|
async function encryptData(data: string, key: CryptoKey): Promise<{ encrypted: ArrayBuffer; iv: Uint8Array }> {
|
|
const encoder = new TextEncoder();
|
|
const encodedData = encoder.encode(data);
|
|
|
|
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
|
|
const encrypted = await window.crypto.subtle.encrypt(
|
|
{
|
|
name: 'AES-GCM',
|
|
iv: iv,
|
|
},
|
|
key,
|
|
encodedData
|
|
);
|
|
|
|
return { encrypted, iv };
|
|
}
|
|
|
|
/**
|
|
* Decrypt data using AES-GCM
|
|
*/
|
|
async function decryptData(encrypted: ArrayBuffer, iv: Uint8Array, key: CryptoKey): Promise<string> {
|
|
const decrypted = await window.crypto.subtle.decrypt(
|
|
{
|
|
name: 'AES-GCM',
|
|
iv: iv,
|
|
},
|
|
key,
|
|
encrypted
|
|
);
|
|
|
|
const decoder = new TextDecoder();
|
|
return decoder.decode(decrypted);
|
|
}
|
|
|
|
/**
|
|
* Export key to base64 string for storage
|
|
*/
|
|
async function exportKey(key: CryptoKey): Promise<string> {
|
|
const exported = await window.crypto.subtle.exportKey('raw', key);
|
|
return btoa(String.fromCharCode(...new Uint8Array(exported)));
|
|
}
|
|
|
|
/**
|
|
* Import key from base64 string
|
|
*/
|
|
async function importKey(base64Key: string): Promise<CryptoKey> {
|
|
const keyData = Uint8Array.from(atob(base64Key), c => c.charCodeAt(0));
|
|
|
|
return await window.crypto.subtle.importKey(
|
|
'raw',
|
|
keyData,
|
|
{
|
|
name: 'AES-GCM',
|
|
},
|
|
true, // extractable
|
|
['encrypt', 'decrypt']
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Store encrypted passport data
|
|
*/
|
|
export async function storeEncryptedPassport(
|
|
profiles: UserProfile[],
|
|
currentUser: UserProfile,
|
|
preference: StoragePreference
|
|
): Promise<void> {
|
|
const dataToStore = {
|
|
profiles,
|
|
currentUser,
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
// Generate encryption key
|
|
const key = await generateEncryptionKey();
|
|
const encryptedData = await encryptData(JSON.stringify(dataToStore), key);
|
|
const exportedKey = await exportKey(key);
|
|
|
|
// Convert encrypted data to base64 for storage
|
|
const encryptedBase64 = btoa(String.fromCharCode(...new Uint8Array(encryptedData.encrypted)));
|
|
const ivBase64 = btoa(String.fromCharCode(...encryptedData.iv));
|
|
|
|
const storageData = {
|
|
encrypted: encryptedBase64,
|
|
iv: ivBase64,
|
|
key: exportedKey,
|
|
};
|
|
|
|
// Store based on preference
|
|
if (preference === 'session') {
|
|
sessionStorage.setItem(STORAGE_KEYS.ENCRYPTED_PASSPORT, JSON.stringify(storageData));
|
|
} else {
|
|
localStorage.setItem(STORAGE_KEYS.ENCRYPTED_PASSPORT, JSON.stringify(storageData));
|
|
}
|
|
|
|
// Store preference
|
|
localStorage.setItem(STORAGE_KEYS.STORAGE_PREFERENCE, preference);
|
|
}
|
|
|
|
/**
|
|
* Retrieve and decrypt passport data
|
|
*/
|
|
export async function retrieveEncryptedPassport(): Promise<{
|
|
profiles: UserProfile[];
|
|
currentUser: UserProfile;
|
|
preference: StoragePreference;
|
|
} | null> {
|
|
// Get storage preference
|
|
const preference = localStorage.getItem(STORAGE_KEYS.STORAGE_PREFERENCE) as StoragePreference | null;
|
|
if (!preference) {
|
|
return null;
|
|
}
|
|
|
|
// Get encrypted data from appropriate storage
|
|
let encryptedData: string | null = null;
|
|
if (preference === 'session') {
|
|
encryptedData = sessionStorage.getItem(STORAGE_KEYS.ENCRYPTED_PASSPORT);
|
|
} else {
|
|
encryptedData = localStorage.getItem(STORAGE_KEYS.ENCRYPTED_PASSPORT);
|
|
}
|
|
|
|
if (!encryptedData) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const { encrypted: encryptedBase64, iv: ivBase64, key: exportedKey } = JSON.parse(encryptedData);
|
|
|
|
// Import key
|
|
const key = await importKey(exportedKey);
|
|
|
|
// Convert from base64
|
|
const encryptedArray = Uint8Array.from(atob(encryptedBase64), c => c.charCodeAt(0));
|
|
const ivArray = Uint8Array.from(atob(ivBase64), c => c.charCodeAt(0));
|
|
|
|
// Decrypt data
|
|
const decryptedData = await decryptData(encryptedArray.buffer, ivArray, key);
|
|
const { profiles, currentUser } = JSON.parse(decryptedData);
|
|
|
|
return { profiles, currentUser, preference };
|
|
} catch (error) {
|
|
console.error('Failed to decrypt stored passport data:', error);
|
|
// Clear corrupted data
|
|
clearStoredPassport();
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear stored passport data
|
|
*/
|
|
export function clearStoredPassport(): void {
|
|
localStorage.removeItem(STORAGE_KEYS.ENCRYPTED_PASSPORT);
|
|
sessionStorage.removeItem(STORAGE_KEYS.ENCRYPTED_PASSPORT);
|
|
localStorage.removeItem(STORAGE_KEYS.STORAGE_PREFERENCE);
|
|
}
|
|
|
|
/**
|
|
* Check if passport data is stored
|
|
*/
|
|
export function hasStoredPassport(): boolean {
|
|
const preference = localStorage.getItem(STORAGE_KEYS.STORAGE_PREFERENCE) as StoragePreference | null;
|
|
if (!preference) {
|
|
return false;
|
|
}
|
|
|
|
if (preference === 'session') {
|
|
return sessionStorage.getItem(STORAGE_KEYS.ENCRYPTED_PASSPORT) !== null;
|
|
} else {
|
|
return localStorage.getItem(STORAGE_KEYS.ENCRYPTED_PASSPORT) !== null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get storage preference
|
|
*/
|
|
export function getStoragePreference(): StoragePreference | null {
|
|
return localStorage.getItem(STORAGE_KEYS.STORAGE_PREFERENCE) as StoragePreference | null;
|
|
} |