diff --git a/browser-extensions/chrome/src/components/Login.tsx b/browser-extensions/chrome/src/components/Login.tsx index f6cc62778..8bb7a2ef8 100644 --- a/browser-extensions/chrome/src/components/Login.tsx +++ b/browser-extensions/chrome/src/components/Login.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import Button from './Button'; -import { srpService } from '../services/SrpService'; +import { srpUtility } from '../utilities/SrpUtility'; +import EncryptionUtility from '../utilities/EncryptionUtility'; const Login: React.FC = () => { const [credentials, setCredentials] = useState({ @@ -16,19 +17,48 @@ const Login: React.FC = () => { try { // 1. Initiate login to get salt and server ephemeral - const loginResponse = await srpService.initiateLogin(credentials.username); + const loginResponse = await srpUtility.initiateLogin(credentials.username); - console.log(credentials); + // 1. Derive key from password using Argon2id + const passwordHashString = await EncryptionUtility.deriveKeyFromPassword( + credentials.password, + loginResponse.salt, + loginResponse.encryptionType, + loginResponse.encryptionSettings + ); // 2. Validate login with SRP protocol - const validationResponse = await srpService.validateLogin( + const validationResponse = await srpUtility.validateLogin( credentials.username, - credentials.password, + passwordHashString, rememberMe, loginResponse ); - console.log(validationResponse); + // Store access and refresh token + if (validationResponse.token) { + localStorage.setItem('accessToken', validationResponse.token!.token); + localStorage.setItem('refreshToken', validationResponse.token!.refreshToken); + } + else { + throw new Error('Login failed -- no token returned'); + } + + // Make another API call trying to get latest vault + const vaultResponse = await fetch('https://localhost:7223/v1/Vault', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('accessToken')}` + } + }); + + const vaultResponseJson = await vaultResponse.json(); + + console.log('Vault response:') + console.log('--------------------------------'); + console.log(vaultResponseJson); + console.log('Encrypted blob:'); + console.log(vaultResponseJson.vault.blob); + // 3. Handle 2FA if required /*if (validationResponse.requiresTwoFactor) { diff --git a/browser-extensions/chrome/src/services/SrpService.tsx b/browser-extensions/chrome/src/services/SrpService.tsx deleted file mode 100644 index 23e964a58..000000000 --- a/browser-extensions/chrome/src/services/SrpService.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import srp from 'secure-remote-password/client' -import argon2 from 'argon2-browser/dist/argon2-bundled.min.js'; -import { Buffer } from 'buffer'; -interface LoginInitiateResponse { - salt: string; - serverEphemeral: string; - encryptionType: string; - encryptionSettings: string; -} - -interface ValidateLoginResponse { - requiresTwoFactor: boolean; - token?: { - token: string; - refreshToken: string; - }; - serverSessionProof: string; -} - -class SrpService { - private static async deriveKeyFromPassword( - password: string, - salt: string, - encryptionType: string = 'Argon2id', - encryptionSettings: string = '{"Iterations":1,"MemorySize":1024,"DegreeOfParallelism":4}' - ): Promise { - const settings = JSON.parse(encryptionSettings); - - try { - if (encryptionType !== 'Argon2Id') { - throw new Error('Unsupported encryption type'); - } - - const hash = await argon2.hash({ - pass: password, - salt: salt, - time: settings.Iterations, - mem: settings.MemorySize, - parallelism: settings.DegreeOfParallelism, - hashLen: 32, - type: 2, // 0 = Argon2d, 1 = Argon2i, 2 = Argon2id - }); - - return hash.hashHex.toUpperCase(); - } catch (error) { - console.error('Argon2 hashing failed:', error); - throw error; - } - } - - public async initiateLogin(username: string): Promise { - const response = await fetch('https://localhost:7223/v1/Auth/login', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ username: username.toLowerCase().trim() }) - }); - - if (!response.ok) { - throw new Error('Login initiation failed'); - } - - return await response.json(); - } - - public async validateLogin( - username: string, - password: string, - rememberMe: boolean, - loginResponse: LoginInitiateResponse - ): Promise { - // 1. Derive key from password - const passwordHashString = await SrpService.deriveKeyFromPassword( - password, - loginResponse.salt, - loginResponse.encryptionType, - loginResponse.encryptionSettings - ); - - console.log('step 1'); - console.log('--------------------------------'); - console.log(passwordHashString); - - // 2. Generate client ephemeral - const clientEphemeral = srp.generateEphemeral() - console.log('step 2'); - console.log('--------------------------------'); - console.log(clientEphemeral); - - // 3. Derive private key - console.log(loginResponse); - const privateKey = srp.derivePrivateKey(loginResponse.salt, username, passwordHashString); - - console.log('step 3'); - console.log('--------------------------------'); - console.log(privateKey); - - // 4. Derive session (simplified for example) - const sessionProof = srp.deriveSession(clientEphemeral.secret, loginResponse.serverEphemeral, loginResponse.salt, username, privateKey); - - console.log('step 4'); - console.log('--------------------------------'); - console.log(sessionProof); - - // 5. Send validation request - const response = await fetch('https://localhost:7223/v1/Auth/validate', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - username: username.toLowerCase().trim(), - rememberMe: rememberMe, - clientPublicEphemeral: clientEphemeral.public, - clientSessionProof: sessionProof.proof, - }) - }); - - const responseJson = await response.json(); - - console.log('Auth response:') - console.log('--------------------------------'); - console.log(responseJson); - - // Store access and refresh token - localStorage.setItem('accessToken', responseJson.token.token); - localStorage.setItem('refreshToken', responseJson.token.refreshToken); - - // Make another API call trying to get latest vault - const vaultResponse = await fetch('https://localhost:7223/v1/Vault', { - headers: { - 'Authorization': `Bearer ${localStorage.getItem('accessToken')}` - } - }); - - const vaultResponseJson = await vaultResponse.json(); - - console.log('Vault response:') - console.log('--------------------------------'); - console.log(vaultResponseJson); - - - return true; - - /* - const passwordHashString = Buffer.from(passwordHash).toString('hex'); - const clientEphemeral = SrpService.generateEphemeral(); - - // 4. Derive session (simplified for example) - const sessionProof = createHash('sha256') - .update(privateKey) - .update(clientEphemeral.secret) - .update(loginResponse.serverEphemeral) - .digest('hex'); - - // 5. Send validation request - const response = await fetch('http://localhost:5092/v1/Auth/validate', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - username: username.toLowerCase().trim(), - rememberMe, - clientPublicEphemeral: clientEphemeral.public, - clientSessionProof: sessionProof - }) - }); - - if (!response.ok) { - throw new Error('Login validation failed'); - } - - return await response.json();*/ - } -} - -export const srpService = new SrpService(); \ No newline at end of file diff --git a/browser-extensions/chrome/src/utilities/EncryptionUtility.tsx b/browser-extensions/chrome/src/utilities/EncryptionUtility.tsx new file mode 100644 index 000000000..731c0de31 --- /dev/null +++ b/browser-extensions/chrome/src/utilities/EncryptionUtility.tsx @@ -0,0 +1,38 @@ +import argon2 from 'argon2-browser/dist/argon2-bundled.min.js'; + +/** + * Utility class for encryption operations which includes Argon2id hashing. + */ +class EncryptionUtility { + public static async deriveKeyFromPassword( + password: string, + salt: string, + encryptionType: string = 'Argon2id', + encryptionSettings: string = '{"Iterations":1,"MemorySize":1024,"DegreeOfParallelism":4}' + ): Promise { + const settings = JSON.parse(encryptionSettings); + + try { + if (encryptionType !== 'Argon2Id') { + throw new Error('Unsupported encryption type'); + } + + const hash = await argon2.hash({ + pass: password, + salt: salt, + time: settings.Iterations, + mem: settings.MemorySize, + parallelism: settings.DegreeOfParallelism, + hashLen: 32, + type: 2, // 0 = Argon2d, 1 = Argon2i, 2 = Argon2id + }); + + return hash.hashHex.toUpperCase(); + } catch (error) { + console.error('Argon2 hashing failed:', error); + throw error; + } + } +} + +export default EncryptionUtility; diff --git a/browser-extensions/chrome/src/utilities/SrpUtility.tsx b/browser-extensions/chrome/src/utilities/SrpUtility.tsx new file mode 100644 index 000000000..d456de059 --- /dev/null +++ b/browser-extensions/chrome/src/utilities/SrpUtility.tsx @@ -0,0 +1,76 @@ +import srp from 'secure-remote-password/client' + +interface LoginInitiateResponse { + salt: string; + serverEphemeral: string; + encryptionType: string; + encryptionSettings: string; +} + +interface ValidateLoginResponse { + requiresTwoFactor: boolean; + token?: { + token: string; + refreshToken: string; + }; + serverSessionProof: string; +} + +/** + * Utility class for SRP authentication operations. + */ +class SrpUtility { + public async initiateLogin(username: string): Promise { + // TODO: make base API URL configurable. The extension will have to support both official + // and self-hosted instances. + const response = await fetch('https://localhost:7223/v1/Auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username: username.toLowerCase().trim() }) + }); + + if (!response.ok) { + throw new Error('Login initiation failed'); + } + + return await response.json(); + } + + public async validateLogin( + username: string, + passwordHashString: string, + rememberMe: boolean, + loginResponse: LoginInitiateResponse + ): Promise { + // 2. Generate client ephemeral + const clientEphemeral = srp.generateEphemeral() + + // 3. Derive private key + const privateKey = srp.derivePrivateKey(loginResponse.salt, username, passwordHashString); + + // 4. Derive session (simplified for example) + const sessionProof = srp.deriveSession(clientEphemeral.secret, loginResponse.serverEphemeral, loginResponse.salt, username, privateKey); + + // 5. Send validation request + // TODO: make base API URL configurable. The extension will have to support both official + // and self-hosted instances. + const response = await fetch('https://localhost:7223/v1/Auth/validate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + username: username.toLowerCase().trim(), + rememberMe: rememberMe, + clientPublicEphemeral: clientEphemeral.public, + clientSessionProof: sessionProof.proof, + }) + }); + + return await response.json(); + } +} + +export const srpUtility = new SrpUtility();