Add argon2id and SRP modules (#541)

This commit is contained in:
Leendert de Borst
2025-01-23 14:14:13 +01:00
parent 33d0b24260
commit f2f8fbbfb6
5 changed files with 417 additions and 10 deletions

View File

@@ -9,8 +9,11 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"argon2-browser": "^1.18.0",
"buffer": "^6.0.3",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"secure-remote-password": "^0.3.1"
},
"devDependencies": {
"@types/react": "^19.0.7",
@@ -1280,6 +1283,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/argon2-browser": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/argon2-browser/-/argon2-browser-1.18.0.tgz",
"integrity": "sha512-ImVAGIItnFnvET1exhsQB7apRztcoC5TnlSqernMJDUjbc/DLq3UEYeXFrLPrlaIl8cVfwnXb6wX2KpFf2zxHw==",
"license": "MIT"
},
"node_modules/array-buffer-to-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/array-buffer-to-hex/-/array-buffer-to-hex-1.0.0.tgz",
"integrity": "sha512-arycdkxgK1cj6s03GDb96tlCxOl1n3kg9M2OHseUc6Pqyqp+lgfceFPmG507eI5V+oxOSEnlOw/dFc7LXBXF4Q==",
"license": "MIT"
},
"node_modules/autoprefixer": {
"version": "10.4.20",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
@@ -1325,6 +1340,26 @@
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -1394,6 +1429,30 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/camelcase-css": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
@@ -1515,6 +1574,21 @@
"node": ">= 8"
}
},
"node_modules/crypto-digest-sync": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/crypto-digest-sync/-/crypto-digest-sync-1.0.0.tgz",
"integrity": "sha512-UQBOB5z+HF4iA8shKQ3PPwhCmdFAihwcytD1Qh4uiz78x04cZZmKtZ1F1VyAjkrA8uEZqXt2tMXfj3dJHtcbng==",
"license": "MIT"
},
"node_modules/crypto-random-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-hex/-/crypto-random-hex-1.0.0.tgz",
"integrity": "sha512-1DuZQ03El13TRgfrqbbjW40Gvi4OKInny/Wxqj23/JMXe214C/3Tlz92bKXWDW3NZT5RjXUGdYW4qiIOUPf+cA==",
"license": "MIT",
"dependencies": {
"array-buffer-to-hex": "^1.0.0"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -1588,6 +1662,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/encode-utf8": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz",
"integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==",
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
@@ -1837,6 +1917,32 @@
"node": ">= 0.4"
}
},
"node_modules/hex-to-array-buffer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/hex-to-array-buffer/-/hex-to-array-buffer-1.1.0.tgz",
"integrity": "sha512-vvl3IM8FfT1uOnHtEqyjkDK9Luqz6MQrH82qIvVnjyXxRhkeaEZyRRPiBgf2yym3nweRVEfayxt/1SoTXZYd4Q==",
"license": "MIT"
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -1949,6 +2055,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/jsbn": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
"license": "MIT"
},
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -2160,6 +2272,12 @@
"dev": true,
"license": "BlueOak-1.0.0"
},
"node_modules/pad-start": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pad-start/-/pad-start-1.0.2.tgz",
"integrity": "sha512-EBN8Ez1SVRcZT1XsIE4WkdnZ5coLoaChkIgAET6gIlaLhXqCz9upVk0DQWFtOYkrpTVvbEppRUnqhTiJrBdkfw==",
"license": "MIT"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -2567,6 +2685,21 @@
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
"license": "MIT"
},
"node_modules/secure-remote-password": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/secure-remote-password/-/secure-remote-password-0.3.1.tgz",
"integrity": "sha512-iEp/qLRfb9XYhfKFrPFfdeD7KVreCjhDKSTRP1G1nRIO0Sw1hjnVHD58ymOhiy9Zf5quHbDIbG9cTupji7qwnA==",
"license": "MIT",
"dependencies": {
"array-buffer-to-hex": "^1.0.0",
"crypto-digest-sync": "^1.0.0",
"crypto-random-hex": "^1.0.0",
"encode-utf8": "^1.0.1",
"hex-to-array-buffer": "^1.1.0",
"jsbn": "^1.1.0",
"pad-start": "^1.0.2"
}
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",

View File

@@ -13,8 +13,11 @@
"license": "ISC",
"description": "",
"dependencies": {
"argon2-browser": "^1.18.0",
"buffer": "^6.0.3",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"secure-remote-password": "^0.3.1"
},
"devDependencies": {
"@types/react": "^19.0.7",

View File

@@ -1,20 +1,27 @@
import React from 'react';
import React, { useState } from 'react';
import '../styles/tailwind.css';
import Button from './Button';
import Login from './Login';
const App: React.FC = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const handleClick = () => {
alert('Button clicked!');
};
return (
<div className="bg-blue-500 text-white p-4">
<h1>Hello, AliasVault Chrome Extension!</h1>
<div className="mt-4">
<Button onClick={handleClick}>
Click me!
</Button>
</div>
<div className="min-h-screen bg-blue-500 items-center justify-center p-4">
<h1 className="text-white text-2xl mb-8">AliasVault</h1>
{isLoggedIn ? (
<div className="mt-4">
<Button onClick={handleClick}>
Click me!
</Button>
</div>
) : (
<Login />
)}
</div>
);
};

View File

@@ -0,0 +1,118 @@
import React, { useState } from 'react';
import Button from './Button';
import { srpService } from '../services/SrpService';
const Login: React.FC = () => {
const [credentials, setCredentials] = useState({
username: '',
password: '',
});
const [rememberMe, setRememberMe] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
try {
// 1. Initiate login to get salt and server ephemeral
const loginResponse = await srpService.initiateLogin(credentials.username);
// 2. Validate login with SRP protocol
const validationResponse = await srpService.validateLogin(
credentials.username,
credentials.password,
rememberMe,
loginResponse
);
console.log(validationResponse);
// 3. Handle 2FA if required
/*if (validationResponse.requiresTwoFactor) {
// TODO: Implement 2FA flow
console.log('2FA required');
return;
}
// 4. Store tokens
if (validationResponse.token) {
localStorage.setItem('accessToken', validationResponse.token.token);
localStorage.setItem('refreshToken', validationResponse.token.refreshToken);
}
// 5. Redirect to home page
window.location.href = '/';*/
} catch (err) {
setError('Login failed. Please check your credentials and try again.');
console.error('Login error:', err);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setCredentials(prev => ({
...prev,
[name]: value
}));
};
return (
<div className="max-w-md">
<form onSubmit={handleSubmit} className="bg-white w-full shadow-md rounded px-8 pt-6 pb-8 mb-4">
{error && (
<div className="mb-4 text-red-500 text-sm">
{error}
</div>
)}
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="username">
Username
</label>
<input
className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="username"
type="text"
name="username"
placeholder="Enter your username"
value={credentials.username}
onChange={handleChange}
required
/>
</div>
<div className="mb-6">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="password">
Password
</label>
<input
className="shadow appearance-none border rounded py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
id="password"
type="password"
name="password"
placeholder="Enter your password"
value={credentials.password}
onChange={handleChange}
required
/>
</div>
<div className="flex items-center justify-between">
<label className="flex items-center">
<input
type="checkbox"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
className="mr-2"
/>
<span className="text-sm">Remember me</span>
</label>
<Button type="submit">
Login
</Button>
</div>
</form>
</div>
);
};
export default Login;

View File

@@ -0,0 +1,146 @@
import srp from 'secure-remote-password/client'
import argon2 from 'argon2-browser/dist/argon2-bundled.min.js';
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":2,"memory":67108864,"parallelism":4}'
): Promise<Uint8Array> {
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.memory / 1024, // Convert bytes to KiB
parallelism: settings.parallelism,
hashLen: 32 // 32 bytes = 256 bits
});
console.log(hash);
return hash.hash;
} catch (error) {
console.error('Argon2 hashing failed:', error);
throw error;
}
}
/**
* Generates a client ephemeral
*
* @returns
*/
private static generateEphemeral(): srp.Ephemeral {
return srp.generateEphemeral()
}
/*private static derivePrivateKey(salt: string, username: string, passwordHash: string): string {
const hash = createHash('sha256')
.update(salt)
.update(username.toLowerCase())
.update(passwordHash)
.digest('hex');
return hash;
}*/
public async initiateLogin(username: string): Promise<LoginInitiateResponse> {
const response = await fetch('http://localhost:5092/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<boolean> {
// Promise<ValidateLoginResponse> {
// 1. Derive key from password
const passwordHash = await SrpService.deriveKeyFromPassword(
password,
loginResponse.salt,
loginResponse.encryptionType,
loginResponse.encryptionSettings
);
return true;
/*
const passwordHashString = Buffer.from(passwordHash).toString('hex');
// 2. Generate client ephemeral
const clientEphemeral = SrpService.generateEphemeral();
// 3. Derive private key
const privateKey = SrpService.derivePrivateKey(
loginResponse.salt,
username,
passwordHashString
);
// 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();