sharenet/frontend/src/lib/auth/storage.ts
continuist dc050d5e34
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
Add self-sovereign passports
2025-10-20 21:15:11 -04:00

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