diff --git a/browser-extensions/chrome/src/background.ts b/browser-extensions/chrome/src/background.ts index dec142f01..a6298e2cc 100644 --- a/browser-extensions/chrome/src/background.ts +++ b/browser-extensions/chrome/src/background.ts @@ -1,14 +1,18 @@ +import { Vault } from './types/webapi/Vault'; import EncryptionUtility from './utils/EncryptionUtility'; import SqliteClient from './utils/SqliteClient'; +import { WebApiService } from './utils/WebApiService'; let vaultState: { derivedKey: string | null; publicEmailDomains: string[]; privateEmailDomains: string[]; + vaultRevisionNumber: number; } = { derivedKey: null, publicEmailDomains: [], - privateEmailDomains: [] + privateEmailDomains: [], + vaultRevisionNumber: 0, }; // Listen for messages from popup @@ -19,6 +23,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { vaultState.derivedKey = message.derivedKey; vaultState.publicEmailDomains = message.publicEmailDomains || []; vaultState.privateEmailDomains = message.privateEmailDomains || []; + vaultState.vaultRevisionNumber = message.vaultRevisionNumber || 0; // Re-encrypt vault with session key (async () : Promise => { @@ -32,7 +37,8 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { chrome.storage.session.set({ encryptedVault, publicEmailDomains: vaultState.publicEmailDomains, - privateEmailDomains: vaultState.privateEmailDomains + privateEmailDomains: vaultState.privateEmailDomains, + vaultRevisionNumber: vaultState.vaultRevisionNumber }, () => { if (chrome.runtime.lastError) { console.error('Failed to store vault:', chrome.runtime.lastError); @@ -54,7 +60,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { return; } - chrome.storage.session.get(['encryptedVault', 'publicEmailDomains', 'privateEmailDomains'], async (result) => { + chrome.storage.session.get(['encryptedVault', 'publicEmailDomains', 'privateEmailDomains', 'vaultRevisionNumber'], async (result) => { try { if (!result.encryptedVault) { console.error('No encrypted vault found in storage'); @@ -73,7 +79,8 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { sendResponse({ vault: decryptedVault, publicEmailDomains: result.publicEmailDomains || [], - privateEmailDomains: result.privateEmailDomains || [] + privateEmailDomains: result.privateEmailDomains || [], + vaultRevisionNumber: result.vaultRevisionNumber || 0 }); } catch (parseError) { console.error('Failed to parse decrypted vault:', parseError); @@ -90,7 +97,8 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { vaultState.derivedKey = null; vaultState.publicEmailDomains = []; vaultState.privateEmailDomains = []; - chrome.storage.session.remove(['encryptedVault', 'publicEmailDomains', 'privateEmailDomains']); + vaultState.vaultRevisionNumber = 0; + chrome.storage.session.remove(['encryptedVault', 'publicEmailDomains', 'privateEmailDomains', 'vaultRevisionNumber']); sendResponse({ success: true }); break; } @@ -231,6 +239,33 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { privateEmailDomains: vaultState.privateEmailDomains }); + // Upload new vault to server + // TODO: extract all required fields by quering the vault db. + const username = await chrome.storage.local.get('username'); + const newVault: Vault = { + blob: encryptedVault, + createdAt: new Date().toISOString(), + credentialsCount: 0, // TODO + currentRevisionNumber: vaultState.vaultRevisionNumber, + emailAddressList: await getEmailAddressesForVault(sqliteClient), + privateEmailDomainList: [], // TODO + publicEmailDomainList: [], // TODO + encryptionPublicKey: '', // TODO + updatedAt: new Date().toISOString(), + username: username.username, // TODO + version: '1.0.0' // TODO + } + + console.log('New vault to upload:', newVault); + + const webApi = new WebApiService( + () => {} + ); + await webApi.initializeBaseUrl(); + await webApi.post('Vault', newVault); + + console.log('Vault uploaded successfully'); + sendResponse({ success: true }); } catch (error) { console.error('Failed to create identity:', error); @@ -242,3 +277,26 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { } return true; }); + +/** + * Get all active email addresses from aliases + * @param sqliteClient The SQLite client + * @returns The list of email addresses + */ +async function getEmailAddressesForVault(sqliteClient: SqliteClient): Promise { + const credentials = sqliteClient.getAllCredentials(); + + // Extract unique email addresses from credentials + const emailAddresses = credentials + .filter(cred => cred.Email != null) + .map(cred => cred.Email!) + .filter((email, index, self) => self.indexOf(email) === index); // Get unique values + + // Filter to only include domains from the private domains list + const filteredEmailAddresses = emailAddresses.filter(email => { + const domain = email.split('@')[1]; + return vaultState.privateEmailDomains.includes(domain); + }); + + return filteredEmailAddresses; +} diff --git a/browser-extensions/chrome/src/context/AuthContext.tsx b/browser-extensions/chrome/src/context/AuthContext.tsx index 1c04179b4..5d7fc65f9 100644 --- a/browser-extensions/chrome/src/context/AuthContext.tsx +++ b/browser-extensions/chrome/src/context/AuthContext.tsx @@ -5,10 +5,7 @@ type AuthContextType = { isInitialized: boolean; username: string | null; login: (username: string, accessToken: string, refreshToken: string) => Promise; - updateTokens: (accessToken: string, refreshToken: string) => Promise; logout: () => Promise; - getAccessToken: () => string | null; - getRefreshToken: () => string | null; } /** @@ -23,94 +20,49 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children const [isLoggedIn, setIsLoggedIn] = useState(false); const [isInitialized, setIsInitialized] = useState(false); const [username, setUsername] = useState(null); - const [, setAccessToken] = useState(null); - const [, setRefreshToken] = useState(null); - const accessTokenRef = useRef(null); - const refreshTokenRef = useRef(null); /** - * Check for tokens in localStorage on initial load + * Check for tokens in chrome storage on initial load */ - useEffect(() : void => { - const storedAccessToken = localStorage.getItem('accessToken'); - const storedRefreshToken = localStorage.getItem('refreshToken'); - const storedUsername = localStorage.getItem('username'); - if (storedAccessToken && storedRefreshToken && storedUsername) { - setAccessToken(storedAccessToken); - setRefreshToken(storedRefreshToken); - setUsername(storedUsername); - setIsLoggedIn(true); - } + useEffect(() => { + const initializeAuth = async () => { + const stored = await chrome.storage.local.get(['accessToken', 'refreshToken', 'username']); + if (stored.accessToken && stored.refreshToken && stored.username) { + setUsername(stored.username); + setIsLoggedIn(true); + } + setIsInitialized(true); + }; - setIsInitialized(true); + initializeAuth(); }, []); /** * Login */ - const login = async (username: string, accessToken: string, refreshToken: string) : Promise => { - accessTokenRef.current = accessToken; // Immediate update - refreshTokenRef.current = refreshToken; // Immediate update - await Promise.all([ - localStorage.setItem('username', username), - localStorage.setItem('accessToken', accessToken), - localStorage.setItem('refreshToken', refreshToken), - ]); + const login = async (username: string, accessToken: string, refreshToken: string) => { + await chrome.storage.local.set({ + username, + accessToken, + refreshToken + }); setUsername(username); - setAccessToken(accessToken); - setRefreshToken(refreshToken); setIsLoggedIn(true); }; - /** - * Update tokens - */ - const updateTokens = async (accessToken: string, refreshToken: string) : Promise => { - accessTokenRef.current = accessToken; // Immediate update - refreshTokenRef.current = refreshToken; // Immediate update - await Promise.all([ - localStorage.setItem('accessToken', accessToken), - localStorage.setItem('refreshToken', refreshToken), - ]); - - setAccessToken(accessToken); - setRefreshToken(refreshToken); - }; - /** * Logout */ - const logout = async () : Promise => { - // Clear vault in background worker. + const logout = async () => { await chrome.runtime.sendMessage({ type: 'CLEAR_VAULT' }); - - await Promise.all([ - localStorage.removeItem('username'), - localStorage.removeItem('accessToken'), - localStorage.removeItem('refreshToken'), - ]); - - await Promise.all([ - setUsername(null), - setAccessToken(null), - setRefreshToken(null), - setIsLoggedIn(false), - ]); + await chrome.storage.local.remove(['username', 'accessToken', 'refreshToken']); + setUsername(null); + setIsLoggedIn(false); }; - /** - * Get access token - */ - const getAccessToken = () : string | null => accessTokenRef.current || localStorage.getItem('accessToken'); - - /** - * Get refresh token - */ - const getRefreshToken = () : string | null => refreshTokenRef.current || localStorage.getItem('refreshToken'); - return ( - + {children} ); diff --git a/browser-extensions/chrome/src/context/DbContext.tsx b/browser-extensions/chrome/src/context/DbContext.tsx index 18867f7bd..429360257 100644 --- a/browser-extensions/chrome/src/context/DbContext.tsx +++ b/browser-extensions/chrome/src/context/DbContext.tsx @@ -5,7 +5,7 @@ type DbContextType = { sqliteClient: SqliteClient | null; dbInitialized: boolean; dbAvailable: boolean; - initializeDatabase: (derivedKey: string, vault: string, publicEmailDomains: string[], privateEmailDomains: string[]) => Promise; + initializeDatabase: (derivedKey: string, vault: string, publicEmailDomains: string[], privateEmailDomains: string[], vaultRevisionNumber: number) => Promise; clearDatabase: () => void; } @@ -40,7 +40,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children } */ const [privateEmailDomains, setPrivateEmailDomains] = useState([]); - const initializeDatabase = useCallback(async (derivedKey: string, vault: string, publicEmailDomains: string[], privateEmailDomains: string[]) => { + const initializeDatabase = useCallback(async (derivedKey: string, vault: string, publicEmailDomains: string[], privateEmailDomains: string[], vaultRevisionNumber: number) => { const client = new SqliteClient(); await client.initializeFromBase64(vault); setSqliteClient(client); @@ -49,13 +49,16 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children } setPublicEmailDomains(publicEmailDomains); setPrivateEmailDomains(privateEmailDomains); - // Store in background worker + // Store in background worker. + // TODO: perhaps we can simply pass the full vaultresponse object instead of the individual fields + // in case we need to access more fields in the future. chrome.runtime.sendMessage({ type: 'STORE_VAULT', derivedKey: derivedKey, vault: vault, publicEmailDomains: publicEmailDomains, - privateEmailDomains: privateEmailDomains + privateEmailDomains: privateEmailDomains, + vaultRevisionNumber: vaultRevisionNumber }); }, []); diff --git a/browser-extensions/chrome/src/context/WebApiContext.tsx b/browser-extensions/chrome/src/context/WebApiContext.tsx index a8d5aa592..850984611 100644 --- a/browser-extensions/chrome/src/context/WebApiContext.tsx +++ b/browser-extensions/chrome/src/context/WebApiContext.tsx @@ -8,7 +8,7 @@ const WebApiContext = createContext(null); * WebApiProvider to provide the WebApiService to the app that components can use. */ export const WebApiProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const { getAccessToken, getRefreshToken, updateTokens, logout } = useAuth(); + const { logout } = useAuth(); const [webApiService, setWebApiService] = useState(null); /** @@ -16,11 +16,6 @@ export const WebApiProvider: React.FC<{ children: React.ReactNode }> = ({ childr */ useEffect(() : void => { const service = new WebApiService( - () => getAccessToken(), - () => getRefreshToken(), - (newAccessToken, newRefreshToken) => { - updateTokens(newAccessToken, newRefreshToken); - }, logout ); setWebApiService(service); @@ -40,7 +35,7 @@ export const WebApiProvider: React.FC<{ children: React.ReactNode }> = ({ childr return () : void => { chrome.storage.onChanged.removeListener(handleStorageChange); }; - }, [getAccessToken, getRefreshToken, updateTokens, logout]); + }, [logout]); if (!webApiService) { return null; diff --git a/browser-extensions/chrome/src/pages/CredentialsList.tsx b/browser-extensions/chrome/src/pages/CredentialsList.tsx index e53f4cda7..8b6b76295 100644 --- a/browser-extensions/chrome/src/pages/CredentialsList.tsx +++ b/browser-extensions/chrome/src/pages/CredentialsList.tsx @@ -48,7 +48,7 @@ const CredentialsList: React.FC = () => { ); // Initialize the SQLite context again with the newly retrieved decrypted blob - await dbContext.initializeDatabase(passwordHashBase64, decryptedBlob); + await dbContext.initializeDatabase(passwordHashBase64, decryptedBlob, vaultResponseJson.vault.publicEmailDomainList, vaultResponseJson.vault.privateEmailDomainList, vaultResponseJson.vault.currentRevisionNumber); } catch (err) { console.error('Refresh error:', err); } finally { diff --git a/browser-extensions/chrome/src/pages/Login.tsx b/browser-extensions/chrome/src/pages/Login.tsx index e40d55074..c359e050f 100644 --- a/browser-extensions/chrome/src/pages/Login.tsx +++ b/browser-extensions/chrome/src/pages/Login.tsx @@ -72,7 +72,7 @@ const Login: React.FC = () => { const decryptedBlob = await EncryptionUtility.symmetricDecrypt(vaultResponseJson.vault.blob, passwordHashBase64); // Initialize the SQLite context with decrypted data - await dbContext.initializeDatabase(passwordHashBase64, decryptedBlob, vaultResponseJson.vault.publicEmailDomainList, vaultResponseJson.vault.privateEmailDomainList); + await dbContext.initializeDatabase(passwordHashBase64, decryptedBlob, vaultResponseJson.vault.publicEmailDomainList, vaultResponseJson.vault.privateEmailDomainList, vaultResponseJson.vault.currentRevisionNumber); // 3. Handle 2FA if required /* diff --git a/browser-extensions/chrome/src/pages/Unlock.tsx b/browser-extensions/chrome/src/pages/Unlock.tsx index 5d7cbf6ba..05ff36775 100644 --- a/browser-extensions/chrome/src/pages/Unlock.tsx +++ b/browser-extensions/chrome/src/pages/Unlock.tsx @@ -53,7 +53,7 @@ const Unlock: React.FC = () => { ); // Initialize the SQLite context with decrypted data - await dbContext.initializeDatabase(passwordHashBase64, decryptedBlob, vaultResponseJson.vault.publicEmailDomainList, vaultResponseJson.vault.privateEmailDomainList); + await dbContext.initializeDatabase(passwordHashBase64, decryptedBlob, vaultResponseJson.vault.publicEmailDomainList, vaultResponseJson.vault.privateEmailDomainList, vaultResponseJson.vault.currentRevisionNumber); } catch (err) { setError('Failed to unlock vault. Please check your password and try again.'); console.error('Unlock error:', err); @@ -69,10 +69,20 @@ const Unlock: React.FC = () => { showLoading(); try { await webApi.logout(); + } catch (err) { + console.error('Logout error:', err); + } + + try { await authContext.logout(); - } finally { + } catch (err) { + console.error('Logout error:', err); + } + finally { hideLoading(); } + + }; return ( diff --git a/browser-extensions/chrome/src/types/webapi/Vault.ts b/browser-extensions/chrome/src/types/webapi/Vault.ts new file mode 100644 index 000000000..f06feaa8e --- /dev/null +++ b/browser-extensions/chrome/src/types/webapi/Vault.ts @@ -0,0 +1,13 @@ +export type Vault = { + blob: string; + createdAt: string; + credentialsCount: number; + currentRevisionNumber: number; + emailAddressList: string[]; + privateEmailDomainList: string[]; + publicEmailDomainList: string[]; + encryptionPublicKey: string; + updatedAt: string; + username: string; + version: string; +} \ No newline at end of file diff --git a/browser-extensions/chrome/src/types/webapi/VaultResponse.ts b/browser-extensions/chrome/src/types/webapi/VaultResponse.ts index 0299574be..80a255b2c 100644 --- a/browser-extensions/chrome/src/types/webapi/VaultResponse.ts +++ b/browser-extensions/chrome/src/types/webapi/VaultResponse.ts @@ -1,18 +1,6 @@ -export type Vault = { - blob: string; - createdAt: string; - credentialsCount: number; - currentRevisionNumber: number; - emailAddressList: string[]; - privateEmailDomainList: string[]; - publicEmailDomainList: string[]; - encryptionPublicKey: string; - updatedAt: string; - username: string; - version: string; - } +import { Vault } from "./Vault"; export type VaultResponse = { - status: number; - vault: Vault; - } \ No newline at end of file + status: number; + vault: Vault; +} \ No newline at end of file diff --git a/browser-extensions/chrome/src/utils/SqliteClient.tsx b/browser-extensions/chrome/src/utils/SqliteClient.tsx index 40b187c9c..23369067d 100644 --- a/browser-extensions/chrome/src/utils/SqliteClient.tsx +++ b/browser-extensions/chrome/src/utils/SqliteClient.tsx @@ -214,21 +214,25 @@ class SqliteClient { const serviceQuery = ` INSERT INTO Services (Id, Name, Url, Logo, CreatedAt, UpdatedAt) VALUES (?, ?, ?, ?, ?, ?)`; - const serviceId = crypto.randomUUID(); + const serviceId = crypto.randomUUID().toUpperCase(); + const currentDateTime = new Date().toISOString() + .replace('T', ' ') + .replace('Z', '') + .substring(0, 23); const serviceResult = this.executeUpdate(serviceQuery, [ serviceId, credential.ServiceName, credential.ServiceUrl ?? null, logoData, - new Date().toISOString(), - new Date().toISOString() + currentDateTime, + currentDateTime ]); // 2. Insert Alias const aliasQuery = ` INSERT INTO Aliases (Id, FirstName, LastName, NickName, BirthDate, Gender, Email, CreatedAt, UpdatedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`; - const aliasId = crypto.randomUUID(); + const aliasId = crypto.randomUUID().toUpperCase(); const aliasResult = this.executeUpdate(aliasQuery, [ aliasId, credential.Alias.FirstName ?? null, @@ -237,23 +241,23 @@ class SqliteClient { credential.Alias.BirthDate ?? null, credential.Alias.Gender ?? null, credential.Alias.Email ?? null, - new Date().toISOString(), - new Date().toISOString() + currentDateTime, + currentDateTime ]); // 3. Insert Credential const credentialQuery = ` INSERT INTO Credentials (Id, Username, Notes, ServiceId, AliasId, CreatedAt, UpdatedAt) VALUES (?, ?, ?, ?, ?, ?, ?)`; - const credentialId = crypto.randomUUID(); + const credentialId = crypto.randomUUID().toUpperCase(); const credentialResult = this.executeUpdate(credentialQuery, [ credentialId, credential.Username, credential.Notes ?? null, serviceId, aliasId, - new Date().toISOString(), - new Date().toISOString() + currentDateTime, + currentDateTime ]); // 4. Insert Password @@ -261,13 +265,13 @@ class SqliteClient { const passwordQuery = ` INSERT INTO Passwords (Id, Value, CredentialId, CreatedAt, UpdatedAt) VALUES (?, ?, ?, ?, ?)`; - const passwordId = crypto.randomUUID(); + const passwordId = crypto.randomUUID().toUpperCase(); this.executeUpdate(passwordQuery, [ passwordId, credential.Password, credentialId, - new Date().toISOString(), - new Date().toISOString() + currentDateTime, + currentDateTime ]); } diff --git a/browser-extensions/chrome/src/utils/WebApiService.ts b/browser-extensions/chrome/src/utils/WebApiService.ts index 677c59f74..dc55b27b1 100644 --- a/browser-extensions/chrome/src/utils/WebApiService.ts +++ b/browser-extensions/chrome/src/utils/WebApiService.ts @@ -17,15 +17,9 @@ export class WebApiService { /** * Constructor for the WebApiService class. * - * @param {Function} getAccessToken - Function to get the access token. - * @param {Function} getRefreshToken - Function to get the refresh token. - * @param {Function} updateTokens - Function to update the access and refresh tokens. * @param {Function} handleLogout - Function to handle logout. */ public constructor( - private getAccessToken: () => string | null, - private getRefreshToken: () => string | null, - private updateTokens: (accessToken: string, refreshToken: string) => void, private handleLogout: () => void ) { // Load the API URL from storage when service is initialized @@ -35,7 +29,7 @@ export class WebApiService { /** * Initialize the base URL for the API from settings. */ - private async initializeBaseUrl() : Promise { + public async initializeBaseUrl() : Promise { const result = await chrome.storage.local.get(['apiUrl']); // Trim trailing slash if present this.baseUrl = (result.apiUrl || 'https://app.aliasvault.net/api').replace(/\/$/, '') + '/v1/'; @@ -53,7 +47,7 @@ export class WebApiService { const headers = new Headers(options.headers || {}); // Add authorization header if we have an access token - const accessToken = this.getAccessToken(); + const accessToken = await this.getAccessToken(); if (accessToken) { headers.set('Authorization', `Bearer ${accessToken}`); } @@ -101,7 +95,7 @@ export class WebApiService { * Refresh the access token. */ private async refreshAccessToken(): Promise { - const refreshToken = this.getRefreshToken(); + const refreshToken = await this.getRefreshToken(); if (!refreshToken) { return null; } @@ -114,7 +108,7 @@ export class WebApiService { 'X-Ignore-Failure': 'true', }, body: JSON.stringify({ - token: this.getAccessToken(), + token: await this.getAccessToken(), refreshToken: refreshToken, }), }); @@ -180,14 +174,42 @@ export class WebApiService { * Logout and revoke tokens via WebApi. */ public async logout(): Promise { - const refreshToken = this.getRefreshToken(); + const refreshToken = await this.getRefreshToken(); if (!refreshToken) { return; } await this.post('Auth/revoke', { - token: this.getAccessToken(), + token: await this.getAccessToken(), refreshToken: refreshToken, }, false); } + + /** + * Get the current access token from storage. + */ + private async getAccessToken(): Promise { + const token = await chrome.storage.local.get('accessToken'); + console.log('accessToken get', token); + return token.accessToken || null; + } + + /** + * Get the current refresh token from storage. + */ + private async getRefreshToken(): Promise { + const token = await chrome.storage.local.get('refreshToken'); + console.log('refreshToken get', token); + return token.refreshToken || null; + } + + /** + * Update both access and refresh tokens in storage. + */ + private async updateTokens(accessToken: string, refreshToken: string): Promise { + await chrome.storage.local.set({ + accessToken, + refreshToken + }); + } } diff --git a/src/AliasVault.Client/Main/Pages/Credentials/View.razor b/src/AliasVault.Client/Main/Pages/Credentials/View.razor index 24c9b93e0..5daf06ea8 100644 --- a/src/AliasVault.Client/Main/Pages/Credentials/View.razor +++ b/src/AliasVault.Client/Main/Pages/Credentials/View.razor @@ -137,7 +137,7 @@ else { // Error loading alias. GlobalNotificationService.AddErrorMessage("This credentials entry does not exist (anymore). Please try again."); - NavigationManager.NavigateTo("/", false, true); + NavigationManager.NavigateTo("/credentials", false, true); return; }