mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-28 19:44:24 -04:00
Implement SRP logic in Rust Core lib, and implement in browser extension and aliasvault.client (#1404)
This commit is contained in:
@@ -64,17 +64,17 @@ class SrpUtility {
|
||||
const srpIdentity = loginResponse.srpIdentity ?? normalizedUsername;
|
||||
|
||||
// Generate client ephemeral
|
||||
const clientEphemeral = SrpAuthService.generateEphemeral();
|
||||
const clientEphemeral = await SrpAuthService.generateEphemeral();
|
||||
|
||||
// Derive private key using srpIdentity (not the typed username)
|
||||
const privateKey = SrpAuthService.derivePrivateKey(
|
||||
const privateKey = await SrpAuthService.derivePrivateKey(
|
||||
loginResponse.salt,
|
||||
srpIdentity,
|
||||
passwordHashString
|
||||
);
|
||||
|
||||
// Derive session using srpIdentity (not the typed username)
|
||||
const session = SrpAuthService.deriveSession(
|
||||
const session = await SrpAuthService.deriveSession(
|
||||
clientEphemeral.secret,
|
||||
loginResponse.serverEphemeral,
|
||||
loginResponse.salt,
|
||||
@@ -127,17 +127,17 @@ class SrpUtility {
|
||||
const srpIdentity = loginResponse.srpIdentity ?? normalizedUsername;
|
||||
|
||||
// Generate client ephemeral
|
||||
const clientEphemeral = SrpAuthService.generateEphemeral();
|
||||
const clientEphemeral = await SrpAuthService.generateEphemeral();
|
||||
|
||||
// Derive private key using srpIdentity (not the typed username)
|
||||
const privateKey = SrpAuthService.derivePrivateKey(
|
||||
const privateKey = await SrpAuthService.derivePrivateKey(
|
||||
loginResponse.salt,
|
||||
srpIdentity,
|
||||
passwordHashString
|
||||
);
|
||||
|
||||
// Derive session using srpIdentity (not the typed username)
|
||||
const session = SrpAuthService.deriveSession(
|
||||
const session = await SrpAuthService.deriveSession(
|
||||
clientEphemeral.secret,
|
||||
loginResponse.serverEphemeral,
|
||||
loginResponse.salt,
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import srp from 'secure-remote-password/client';
|
||||
import { browser } from 'wxt/browser';
|
||||
|
||||
import initWasm, {
|
||||
srpGenerateSalt,
|
||||
srpDerivePrivateKey,
|
||||
srpDeriveVerifier,
|
||||
srpGenerateEphemeral,
|
||||
srpDeriveSession,
|
||||
} from '../dist/core/rust/aliasvault_core.js';
|
||||
|
||||
import type { TokenModel, LoginResponse, BadRequestResponse } from '@/utils/dist/core/models/webapi';
|
||||
|
||||
@@ -50,16 +58,72 @@ export const DEFAULT_ENCRYPTION = {
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* SrpAuthService provides SRP-based authentication utilities.
|
||||
* SRP ephemeral key pair type.
|
||||
*/
|
||||
type SrpEphemeral = {
|
||||
public: string;
|
||||
secret: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* SRP session type.
|
||||
*/
|
||||
type SrpSession = {
|
||||
proof: string;
|
||||
key: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* SrpAuthService provides SRP-based authentication utilities using Rust WASM.
|
||||
*
|
||||
* This service handles:
|
||||
* - User registration with SRP protocol
|
||||
* - Password hashing and key derivation
|
||||
* - SRP verifier generation
|
||||
*
|
||||
* It is designed to be used by both the browser extension UI and E2E tests.
|
||||
* It uses the Rust core library compiled to WASM for cross-platform consistency.
|
||||
* The WASM module must be initialized before use (handled automatically).
|
||||
*/
|
||||
export class SrpAuthService {
|
||||
private static wasmInitialized = false;
|
||||
private static wasmInitPromise: Promise<void> | null = null;
|
||||
|
||||
/**
|
||||
* Initialize the Rust WASM module.
|
||||
* Called automatically by methods that require WASM.
|
||||
* Safe to call multiple times - only initializes once.
|
||||
*/
|
||||
private static async initWasm(): Promise<void> {
|
||||
if (this.wasmInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure we only initialize once even with concurrent calls
|
||||
if (this.wasmInitPromise) {
|
||||
return this.wasmInitPromise;
|
||||
}
|
||||
|
||||
this.wasmInitPromise = (async (): Promise<void> => {
|
||||
try {
|
||||
/*
|
||||
* Fetch WASM bytes using browser.runtime.getURL for correct extension path.
|
||||
* Cast to string to bypass WXT's strict PublicPath typing.
|
||||
*/
|
||||
const wasmUrl = (browser.runtime.getURL as (path: string) => string)('src/aliasvault_core_bg.wasm');
|
||||
const wasmResponse = await fetch(wasmUrl);
|
||||
const wasmBytes = await wasmResponse.arrayBuffer();
|
||||
// Pass as object to avoid deprecation warning from wasm-bindgen
|
||||
await initWasm({ module_or_path: wasmBytes });
|
||||
this.wasmInitialized = true;
|
||||
} catch (error) {
|
||||
this.wasmInitPromise = null;
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
|
||||
return this.wasmInitPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a username by converting to lowercase and trimming whitespace.
|
||||
*
|
||||
@@ -71,12 +135,13 @@ export class SrpAuthService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a cryptographically secure SRP salt.
|
||||
* Generates a cryptographically secure SRP salt using Rust WASM.
|
||||
*
|
||||
* @returns A random salt string
|
||||
* @returns A random salt string (uppercase hex)
|
||||
*/
|
||||
public static generateSalt(): string {
|
||||
return srp.generateSalt();
|
||||
public static async generateSalt(): Promise<string> {
|
||||
await this.initWasm();
|
||||
return srpGenerateSalt();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,64 +168,68 @@ export class SrpAuthService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives an SRP private key from credentials.
|
||||
* Derives an SRP private key from credentials using Rust WASM.
|
||||
*
|
||||
* @param salt - The SRP salt
|
||||
* @param username - The normalized username
|
||||
* @param username - The normalized username or SRP identity
|
||||
* @param passwordHashString - The password hash as uppercase hex string
|
||||
* @returns The SRP private key
|
||||
* @returns The SRP private key (uppercase hex)
|
||||
*/
|
||||
public static derivePrivateKey(
|
||||
public static async derivePrivateKey(
|
||||
salt: string,
|
||||
username: string,
|
||||
passwordHashString: string
|
||||
): string {
|
||||
return srp.derivePrivateKey(salt, SrpAuthService.normalizeUsername(username), passwordHashString);
|
||||
): Promise<string> {
|
||||
await this.initWasm();
|
||||
return srpDerivePrivateKey(salt, SrpAuthService.normalizeUsername(username), passwordHashString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives an SRP verifier from a private key.
|
||||
* Derives an SRP verifier from a private key using Rust WASM.
|
||||
*
|
||||
* @param privateKey - The SRP private key
|
||||
* @returns The SRP verifier
|
||||
* @returns The SRP verifier (uppercase hex)
|
||||
*/
|
||||
public static deriveVerifier(privateKey: string): string {
|
||||
return srp.deriveVerifier(privateKey);
|
||||
public static async deriveVerifier(privateKey: string): Promise<string> {
|
||||
await this.initWasm();
|
||||
return srpDeriveVerifier(privateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an SRP ephemeral key pair for client-side authentication.
|
||||
* Generates an SRP ephemeral key pair for client-side authentication using Rust WASM.
|
||||
*
|
||||
* @returns Object containing public and secret ephemeral values
|
||||
* @returns Object containing public and secret ephemeral values (uppercase hex)
|
||||
*/
|
||||
public static generateEphemeral(): { public: string; secret: string } {
|
||||
return srp.generateEphemeral();
|
||||
public static async generateEphemeral(): Promise<SrpEphemeral> {
|
||||
await this.initWasm();
|
||||
return srpGenerateEphemeral() as SrpEphemeral;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives an SRP session from the authentication exchange.
|
||||
* Derives an SRP session from the authentication exchange using Rust WASM.
|
||||
*
|
||||
* @param clientSecretEphemeral - Client's secret ephemeral value
|
||||
* @param serverPublicEphemeral - Server's public ephemeral value
|
||||
* @param salt - The SRP salt
|
||||
* @param username - The normalized username
|
||||
* @param username - The normalized username or SRP identity
|
||||
* @param privateKey - The SRP private key
|
||||
* @returns The SRP session containing proof and key
|
||||
* @returns The SRP session containing proof and key (uppercase hex)
|
||||
*/
|
||||
public static deriveSession(
|
||||
public static async deriveSession(
|
||||
clientSecretEphemeral: string,
|
||||
serverPublicEphemeral: string,
|
||||
salt: string,
|
||||
username: string,
|
||||
privateKey: string
|
||||
): { proof: string; key: string } {
|
||||
return srp.deriveSession(
|
||||
): Promise<SrpSession> {
|
||||
await this.initWasm();
|
||||
return srpDeriveSession(
|
||||
clientSecretEphemeral,
|
||||
serverPublicEphemeral,
|
||||
salt,
|
||||
SrpAuthService.normalizeUsername(username),
|
||||
privateKey
|
||||
);
|
||||
) as SrpSession;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -221,7 +290,7 @@ export class SrpAuthService {
|
||||
password: string
|
||||
): Promise<RegisterRequest> {
|
||||
const normalizedUsername = SrpAuthService.normalizeUsername(username);
|
||||
const salt = SrpAuthService.generateSalt();
|
||||
const salt = await SrpAuthService.generateSalt();
|
||||
|
||||
/**
|
||||
* Generate a random GUID for SRP identity. This is used for all SRP operations,
|
||||
@@ -238,8 +307,8 @@ export class SrpAuthService {
|
||||
);
|
||||
|
||||
// Generate SRP private key and verifier using srpIdentity (not username)
|
||||
const privateKey = SrpAuthService.derivePrivateKey(salt, srpIdentity, credentials.passwordHashString);
|
||||
const verifier = SrpAuthService.deriveVerifier(privateKey);
|
||||
const privateKey = await SrpAuthService.derivePrivateKey(salt, srpIdentity, credentials.passwordHashString);
|
||||
const verifier = await SrpAuthService.deriveVerifier(privateKey);
|
||||
|
||||
return {
|
||||
username: normalizedUsername,
|
||||
@@ -362,13 +431,13 @@ export class SrpAuthService {
|
||||
);
|
||||
|
||||
// Step 3: Generate SRP session using srpIdentity (not the typed username)
|
||||
const clientEphemeral = SrpAuthService.generateEphemeral();
|
||||
const privateKey = SrpAuthService.derivePrivateKey(
|
||||
const clientEphemeral = await SrpAuthService.generateEphemeral();
|
||||
const privateKey = await SrpAuthService.derivePrivateKey(
|
||||
loginResponse.salt,
|
||||
srpIdentity,
|
||||
credentials.passwordHashString
|
||||
);
|
||||
const session = SrpAuthService.deriveSession(
|
||||
const session = await SrpAuthService.deriveSession(
|
||||
clientEphemeral.secret,
|
||||
loginResponse.serverEphemeral,
|
||||
loginResponse.salt,
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
@using AliasVault.Client.Auth.Models
|
||||
@using AliasVault.Client.Utilities
|
||||
@using AliasVault.Cryptography.Client
|
||||
@using SecureRemotePassword
|
||||
@using AliasVault.Client.Services.JsInterop.RustCore
|
||||
@using Microsoft.Extensions.Localization
|
||||
@inject SrpService SrpService
|
||||
|
||||
@if (_showTwoFactorAuthStep)
|
||||
{
|
||||
@@ -271,9 +272,9 @@ else
|
||||
_passwordHash = await Encryption.DeriveKeyFromPasswordAsync(_loginModel.Password, loginResponse.Salt, loginResponse.EncryptionType, loginResponse.EncryptionSettings);
|
||||
var passwordHashString = BitConverter.ToString(_passwordHash).Replace("-", string.Empty);
|
||||
|
||||
_clientEphemeral = Srp.GenerateEphemeralClient();
|
||||
var privateKey = Srp.DerivePrivateKey(loginResponse.Salt, srpIdentity, passwordHashString);
|
||||
_clientSession = Srp.DeriveSessionClient(
|
||||
_clientEphemeral = await SrpService.GenerateEphemeralAsync();
|
||||
var privateKey = await SrpService.DerivePrivateKeyAsync(loginResponse.Salt, srpIdentity, passwordHashString);
|
||||
_clientSession = await SrpService.DeriveSessionClientAsync(
|
||||
privateKey,
|
||||
_clientEphemeral.Secret,
|
||||
loginResponse.ServerEphemeral,
|
||||
@@ -431,7 +432,7 @@ else
|
||||
private async Task<List<string>> ProcessLoginVerify(ValidateLoginResponse validateLoginResponse)
|
||||
{
|
||||
// 5. Client verifies proof.
|
||||
Srp.VerifySession(_clientEphemeral.Public, _clientSession, validateLoginResponse.ServerSessionProof);
|
||||
await SrpService.VerifySessionAsync(_clientEphemeral.Public, _clientSession, validateLoginResponse.ServerSessionProof);
|
||||
|
||||
// Store the tokens in local storage.
|
||||
await AuthService.StoreAccessTokenAsync(validateLoginResponse.Token!.Token);
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
@using AliasVault.Client.Utilities
|
||||
@using AliasVault.Cryptography.Client
|
||||
@using AliasVault.Shared.Models.WebApi.Auth
|
||||
@using SecureRemotePassword
|
||||
|
||||
<div class="w-full mx-auto">
|
||||
<div class="relative inset-0 mt-10 z-10">
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
@using AliasVault.Shared.Models.WebApi.PasswordChange
|
||||
@using AliasVault.Shared.Models.WebApi.Vault;
|
||||
@using AliasVault.Cryptography.Client
|
||||
@using SecureRemotePassword
|
||||
@using AliasVault.Client.Services.JsInterop.RustCore
|
||||
@using Microsoft.Extensions.Localization
|
||||
@inherits MainBase
|
||||
@inject HttpClient Http
|
||||
@inject SrpService SrpService
|
||||
|
||||
<LayoutPageTitle>@Localizer["PageTitle"]</LayoutPageTitle>
|
||||
|
||||
@@ -101,6 +102,7 @@ else
|
||||
|
||||
private SrpEphemeral ClientEphemeral = new();
|
||||
private SrpSession ClientSession = new();
|
||||
private string PrivateKey = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -163,24 +165,23 @@ else
|
||||
var currentPasswordHash = await Encryption.DeriveKeyFromPasswordAsync(PasswordChangeFormModel.CurrentPassword, CurrentSalt, CurrentEncryptionType, CurrentEncryptionSettings);
|
||||
var currentPasswordHashString = BitConverter.ToString(currentPasswordHash).Replace("-", string.Empty);
|
||||
|
||||
ClientEphemeral = Srp.GenerateEphemeralClient();
|
||||
ClientEphemeral = await SrpService.GenerateEphemeralAsync();
|
||||
var username = await GetUsernameAsync();
|
||||
|
||||
// Use srpIdentity from server response if available, otherwise fall back to username.
|
||||
// Note: the fallback can be removed in the future after 0.26.0+ is deployed.
|
||||
var srpIdentity = CurrentSrpIdentity ?? username.ToLowerInvariant();
|
||||
|
||||
var privateKey = Srp.DerivePrivateKey(CurrentSalt, srpIdentity, currentPasswordHashString);
|
||||
ClientSession = Srp.DeriveSessionClient(
|
||||
privateKey,
|
||||
PrivateKey = await SrpService.DerivePrivateKeyAsync(CurrentSalt, srpIdentity, currentPasswordHashString);
|
||||
ClientSession = await SrpService.DeriveSessionClientAsync(
|
||||
PrivateKey,
|
||||
ClientEphemeral.Secret,
|
||||
CurrentServerEphemeral,
|
||||
CurrentSalt,
|
||||
srpIdentity);
|
||||
|
||||
// Generate salt and verifier for new password.
|
||||
var client = new SrpClient();
|
||||
var newSalt = client.GenerateSalt();
|
||||
// Generate salt and verifier for new password using Rust WASM.
|
||||
var newSalt = await SrpService.GenerateSaltAsync();
|
||||
|
||||
byte[] newPasswordHash = await Encryption.DeriveKeyFromPasswordAsync(PasswordChangeFormModel.NewPassword, newSalt);
|
||||
var newPasswordHashString = BitConverter.ToString(newPasswordHash).Replace("-", string.Empty);
|
||||
@@ -193,7 +194,7 @@ else
|
||||
await AuthService.StoreEncryptionKeyAsync(newPasswordHash);
|
||||
|
||||
// Use srpIdentity for generating the new verifier to maintain consistency.
|
||||
var srpPasswordChange = Srp.PasswordChangeAsync(client, newSalt, srpIdentity, newPasswordHashString);
|
||||
var (srpSalt, srpVerifier) = await SrpService.PreparePasswordChangeAsync(srpIdentity, newPasswordHashString);
|
||||
|
||||
// Prepare new vault model to update to.
|
||||
var encryptedBase64String = await DbService.GetEncryptedDatabaseBase64String();
|
||||
@@ -215,8 +216,8 @@ else
|
||||
UpdatedAt = vault.UpdatedAt,
|
||||
CurrentClientPublicEphemeral = ClientEphemeral.Public,
|
||||
CurrentClientSessionProof = ClientSession.Proof,
|
||||
NewPasswordSalt = srpPasswordChange.Salt,
|
||||
NewPasswordVerifier = srpPasswordChange.Verifier
|
||||
NewPasswordSalt = srpSalt,
|
||||
NewPasswordVerifier = srpVerifier
|
||||
};
|
||||
|
||||
// Clear form.
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
@using AliasVault.Client.Utilities
|
||||
@using AliasVault.Shared.Models.WebApi.Auth
|
||||
@using AliasVault.Cryptography.Client
|
||||
@using SecureRemotePassword
|
||||
@using AliasVault.Client.Services.JsInterop.RustCore
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Microsoft.Extensions.Localization
|
||||
@using AliasVault.Client.Resources
|
||||
@inject HttpClient Http
|
||||
@inject SrpService SrpService
|
||||
|
||||
<LayoutPageTitle>@Localizer["PageTitle"]</LayoutPageTitle>
|
||||
|
||||
@@ -99,12 +100,17 @@
|
||||
/// <summary>
|
||||
/// The ephemeral client for SRP.
|
||||
/// </summary>
|
||||
private SrpEphemeral ClientEphemeral { get; set; } = null!;
|
||||
private SrpEphemeral ClientEphemeral { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The session client for SRP.
|
||||
/// </summary>
|
||||
private SrpSession ClientSession { get; set; } = null!;
|
||||
private SrpSession ClientSession { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The SRP private key.
|
||||
/// </summary>
|
||||
private string PrivateKey { get; set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -179,10 +185,10 @@
|
||||
var passwordHash = await Encryption.DeriveKeyFromPasswordAsync(_passwordModel.Password, loginResponse.Salt, loginResponse.EncryptionType, loginResponse.EncryptionSettings);
|
||||
var passwordHashString = BitConverter.ToString(passwordHash).Replace("-", string.Empty);
|
||||
|
||||
ClientEphemeral = Srp.GenerateEphemeralClient();
|
||||
var privateKey = Srp.DerivePrivateKey(loginResponse.Salt, srpIdentity, passwordHashString);
|
||||
ClientSession = Srp.DeriveSessionClient(
|
||||
privateKey,
|
||||
ClientEphemeral = await SrpService.GenerateEphemeralAsync();
|
||||
PrivateKey = await SrpService.DerivePrivateKeyAsync(loginResponse.Salt, srpIdentity, passwordHashString);
|
||||
ClientSession = await SrpService.DeriveSessionClientAsync(
|
||||
PrivateKey,
|
||||
ClientEphemeral.Secret,
|
||||
loginResponse.ServerEphemeral,
|
||||
loginResponse.Salt,
|
||||
|
||||
@@ -102,6 +102,7 @@ builder.Services.AddScoped<ConfirmModalService>();
|
||||
builder.Services.AddScoped<QuickCreateStateService>();
|
||||
builder.Services.AddScoped<LanguageService>();
|
||||
builder.Services.AddScoped<RustCoreService>();
|
||||
builder.Services.AddScoped<SrpService>();
|
||||
|
||||
builder.Services.AddAuthorizationCore();
|
||||
builder.Services.AddBlazoredLocalStorage();
|
||||
|
||||
@@ -9,12 +9,12 @@ namespace AliasVault.Client.Services.Auth;
|
||||
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using AliasVault.Client.Services.JsInterop.RustCore;
|
||||
using AliasVault.Client.Utilities;
|
||||
using AliasVault.Cryptography.Client;
|
||||
using AliasVault.Shared.Models.WebApi.Auth;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using SecureRemotePassword;
|
||||
|
||||
/// <summary>
|
||||
/// Service responsible for handling user registration operations.
|
||||
@@ -24,7 +24,8 @@ using SecureRemotePassword;
|
||||
/// <param name="authService">The service handling authentication operations.</param>
|
||||
/// <param name="config">The application configuration.</param>
|
||||
/// <param name="localizerFactory">The string localizer factory for localization.</param>
|
||||
public class UserRegistrationService(HttpClient httpClient, AuthenticationStateProvider authStateProvider, AuthService authService, Config config, IStringLocalizerFactory localizerFactory)
|
||||
/// <param name="srpService">The SRP service for secure authentication.</param>
|
||||
public class UserRegistrationService(HttpClient httpClient, AuthenticationStateProvider authStateProvider, AuthService authService, Config config, IStringLocalizerFactory localizerFactory, SrpService srpService)
|
||||
{
|
||||
private readonly IStringLocalizer _apiErrorLocalizer = localizerFactory.Create("ApiErrors", "AliasVault.Client");
|
||||
|
||||
@@ -38,9 +39,6 @@ public class UserRegistrationService(HttpClient httpClient, AuthenticationStateP
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = new SrpClient();
|
||||
var salt = client.GenerateSalt();
|
||||
|
||||
// Generate a random GUID for SRP identity. This is used for all SRP operations,
|
||||
// is set during registration, and never changes.
|
||||
var srpIdentity = Guid.NewGuid().ToString();
|
||||
@@ -53,11 +51,16 @@ public class UserRegistrationService(HttpClient httpClient, AuthenticationStateP
|
||||
encryptionSettings = config.CryptographyOverrideSettings;
|
||||
}
|
||||
|
||||
// Generate salt using Rust WASM
|
||||
var salt = await srpService.GenerateSaltAsync();
|
||||
|
||||
var passwordHash = await Encryption.DeriveKeyFromPasswordAsync(password, salt, encryptionType, encryptionSettings);
|
||||
var passwordHashString = BitConverter.ToString(passwordHash).Replace("-", string.Empty);
|
||||
var srpSignup = Srp.PasswordChangeAsync(client, salt, srpIdentity, passwordHashString);
|
||||
|
||||
var registerRequest = new RegisterRequest(username, srpSignup.Salt, srpSignup.Verifier, encryptionType, encryptionSettings, srpIdentity);
|
||||
// Generate verifier using Rust WASM
|
||||
var (srpSalt, srpVerifier) = await srpService.PreparePasswordChangeAsync(srpIdentity, passwordHashString);
|
||||
|
||||
var registerRequest = new RegisterRequest(username, srpSalt, srpVerifier, encryptionType, encryptionSettings, srpIdentity);
|
||||
var result = await httpClient.PostAsJsonAsync("v1/Auth/register", registerRequest);
|
||||
var responseContent = await result.Content.ReadAsStringAsync();
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="SrpEphemeral.cs" company="aliasvault">
|
||||
// Copyright (c) aliasvault. All rights reserved.
|
||||
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Client.Services.JsInterop.RustCore;
|
||||
|
||||
/// <summary>
|
||||
/// SRP Ephemeral keypair with public and secret components.
|
||||
/// </summary>
|
||||
public class SrpEphemeral
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the public ephemeral value (uppercase hex string).
|
||||
/// </summary>
|
||||
public string Public { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the secret ephemeral value (uppercase hex string).
|
||||
/// </summary>
|
||||
public string Secret { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="SrpService.cs" company="aliasvault">
|
||||
// Copyright (c) aliasvault. All rights reserved.
|
||||
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Client.Services.JsInterop.RustCore;
|
||||
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
/// <summary>
|
||||
/// JavaScript interop wrapper for the Rust WASM SRP (Secure Remote Password) library.
|
||||
/// Provides SRP authentication functionality via WASM.
|
||||
/// </summary>
|
||||
public class SrpService : IAsyncDisposable
|
||||
{
|
||||
private readonly IJSRuntime jsRuntime;
|
||||
private readonly RustCoreService rustCoreService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SrpService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="jsRuntime">The JS runtime for interop.</param>
|
||||
/// <param name="rustCoreService">The Rust core service for WASM availability checks.</param>
|
||||
public SrpService(IJSRuntime jsRuntime, RustCoreService rustCoreService)
|
||||
{
|
||||
this.jsRuntime = jsRuntime;
|
||||
this.rustCoreService = rustCoreService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a random salt for SRP registration.
|
||||
/// </summary>
|
||||
/// <returns>64-character uppercase hex string (32 bytes).</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if WASM module is unavailable.</exception>
|
||||
public async Task<string> GenerateSaltAsync()
|
||||
{
|
||||
if (!await rustCoreService.WaitForAvailabilityAsync())
|
||||
{
|
||||
throw new InvalidOperationException("Rust WASM module is not available.");
|
||||
}
|
||||
|
||||
return await jsRuntime.InvokeAsync<string>("rustCoreSrpGenerateSalt");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Derive a private key from salt, identity, and password hash.
|
||||
/// </summary>
|
||||
/// <param name="salt">The salt (hex string).</param>
|
||||
/// <param name="identity">The SRP identity (username or GUID), will be lowercased.</param>
|
||||
/// <param name="passwordHash">The password hash (hex string).</param>
|
||||
/// <returns>64-character uppercase hex string (32 bytes).</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if WASM module is unavailable.</exception>
|
||||
public async Task<string> DerivePrivateKeyAsync(string salt, string identity, string passwordHash)
|
||||
{
|
||||
if (!await rustCoreService.WaitForAvailabilityAsync())
|
||||
{
|
||||
throw new InvalidOperationException("Rust WASM module is not available.");
|
||||
}
|
||||
|
||||
// Make sure the identity is lowercase as the SRP protocol is case sensitive.
|
||||
identity = identity.ToLowerInvariant();
|
||||
|
||||
return await jsRuntime.InvokeAsync<string>("rustCoreSrpDerivePrivateKey", salt, identity, passwordHash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Derive a verifier from a private key.
|
||||
/// </summary>
|
||||
/// <param name="privateKey">The private key (hex string).</param>
|
||||
/// <returns>512-character uppercase hex string (256 bytes).</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if WASM module is unavailable.</exception>
|
||||
public async Task<string> DeriveVerifierAsync(string privateKey)
|
||||
{
|
||||
if (!await rustCoreService.WaitForAvailabilityAsync())
|
||||
{
|
||||
throw new InvalidOperationException("Rust WASM module is not available.");
|
||||
}
|
||||
|
||||
return await jsRuntime.InvokeAsync<string>("rustCoreSrpDeriveVerifier", privateKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate client ephemeral keypair.
|
||||
/// </summary>
|
||||
/// <returns>Ephemeral object with Public and Secret hex strings.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if WASM module is unavailable.</exception>
|
||||
public async Task<SrpEphemeral> GenerateEphemeralAsync()
|
||||
{
|
||||
if (!await rustCoreService.WaitForAvailabilityAsync())
|
||||
{
|
||||
throw new InvalidOperationException("Rust WASM module is not available.");
|
||||
}
|
||||
|
||||
return await jsRuntime.InvokeAsync<SrpEphemeral>("rustCoreSrpGenerateEphemeral");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Derive client session from ephemeral values.
|
||||
/// </summary>
|
||||
/// <param name="clientSecret">Client ephemeral secret (hex string).</param>
|
||||
/// <param name="serverPublic">Server ephemeral public (hex string).</param>
|
||||
/// <param name="salt">The salt (hex string).</param>
|
||||
/// <param name="identity">The SRP identity, will be lowercased.</param>
|
||||
/// <param name="privateKey">The private key (hex string).</param>
|
||||
/// <returns>Session object with Key and Proof hex strings.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if WASM module is unavailable.</exception>
|
||||
public async Task<SrpSession> DeriveSessionAsync(string clientSecret, string serverPublic, string salt, string identity, string privateKey)
|
||||
{
|
||||
if (!await rustCoreService.WaitForAvailabilityAsync())
|
||||
{
|
||||
throw new InvalidOperationException("Rust WASM module is not available.");
|
||||
}
|
||||
|
||||
// Make sure the identity is lowercase as the SRP protocol is case sensitive.
|
||||
identity = identity.ToLowerInvariant();
|
||||
|
||||
return await jsRuntime.InvokeAsync<SrpSession>("rustCoreSrpDeriveSession", clientSecret, serverPublic, salt, identity, privateKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepare password change/registration by generating salt and verifier.
|
||||
/// </summary>
|
||||
/// <param name="identity">The SRP identity (username or GUID).</param>
|
||||
/// <param name="passwordHashString">The password hash as hex string.</param>
|
||||
/// <returns>Tuple with Salt and Verifier.</returns>
|
||||
public async Task<(string Salt, string Verifier)> PreparePasswordChangeAsync(string identity, string passwordHashString)
|
||||
{
|
||||
var salt = await GenerateSaltAsync();
|
||||
var privateKey = await DerivePrivateKeyAsync(salt, identity, passwordHashString);
|
||||
var verifier = await DeriveVerifierAsync(privateKey);
|
||||
|
||||
return (salt, verifier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Derive session client-side (convenience method matching old Srp.DeriveSessionClient signature).
|
||||
/// </summary>
|
||||
/// <param name="privateKey">The private key.</param>
|
||||
/// <param name="clientSecretEphemeral">Client ephemeral secret.</param>
|
||||
/// <param name="serverEphemeralPublic">Server public ephemeral.</param>
|
||||
/// <param name="salt">Salt.</param>
|
||||
/// <param name="identity">Identity.</param>
|
||||
/// <returns>SrpSession.</returns>
|
||||
public async Task<SrpSession> DeriveSessionClientAsync(string privateKey, string clientSecretEphemeral, string serverEphemeralPublic, string salt, string identity)
|
||||
{
|
||||
return await DeriveSessionAsync(clientSecretEphemeral, serverEphemeralPublic, salt, identity, privateKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify the server's session proof (M2) on the client side.
|
||||
/// </summary>
|
||||
/// <param name="clientPublic">Client public ephemeral (A).</param>
|
||||
/// <param name="clientSession">Client session containing proof (M1) and key (K).</param>
|
||||
/// <param name="serverProof">Server proof (M2) to verify.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if WASM module is unavailable.</exception>
|
||||
/// <exception cref="System.Security.SecurityException">Thrown if verification fails.</exception>
|
||||
public async Task VerifySessionAsync(string clientPublic, SrpSession clientSession, string serverProof)
|
||||
{
|
||||
if (!await rustCoreService.WaitForAvailabilityAsync())
|
||||
{
|
||||
throw new InvalidOperationException("Rust WASM module is not available.");
|
||||
}
|
||||
|
||||
var result = await jsRuntime.InvokeAsync<bool>("rustCoreSrpVerifySession", clientPublic, clientSession.Proof, clientSession.Key, serverProof);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
throw new System.Security.SecurityException("Server session proof verification failed.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="SrpSession.cs" company="aliasvault">
|
||||
// Copyright (c) aliasvault. All rights reserved.
|
||||
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Client.Services.JsInterop.RustCore;
|
||||
|
||||
/// <summary>
|
||||
/// SRP Session with key and proof.
|
||||
/// </summary>
|
||||
public class SrpSession
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the session key (uppercase hex string).
|
||||
/// </summary>
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the session proof (uppercase hex string).
|
||||
/// </summary>
|
||||
public string Proof { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -230,3 +230,130 @@ window.rustCorePruneVault = async function(inputJson) {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// SRP (Secure Remote Password) Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Generate a random salt for SRP registration.
|
||||
* @returns {Promise<string>} 64-character uppercase hex string (32 bytes).
|
||||
*/
|
||||
window.rustCoreSrpGenerateSalt = async function() {
|
||||
if (!await initRustCore()) {
|
||||
throw new Error('Rust WASM module not available');
|
||||
}
|
||||
|
||||
try {
|
||||
return wasmModule.srpGenerateSalt();
|
||||
} catch (error) {
|
||||
console.error('[RustCore] SRP generate salt failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Derive a private key from salt, identity, and password hash.
|
||||
* @param {string} salt - The salt (hex string).
|
||||
* @param {string} identity - The SRP identity (username or GUID).
|
||||
* @param {string} passwordHash - The password hash (hex string).
|
||||
* @returns {Promise<string>} 64-character uppercase hex string (32 bytes).
|
||||
*/
|
||||
window.rustCoreSrpDerivePrivateKey = async function(salt, identity, passwordHash) {
|
||||
if (!await initRustCore()) {
|
||||
throw new Error('Rust WASM module not available');
|
||||
}
|
||||
|
||||
try {
|
||||
return wasmModule.srpDerivePrivateKey(salt, identity, passwordHash);
|
||||
} catch (error) {
|
||||
console.error('[RustCore] SRP derive private key failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Derive a verifier from a private key.
|
||||
* @param {string} privateKey - The private key (hex string).
|
||||
* @returns {Promise<string>} 512-character uppercase hex string (256 bytes).
|
||||
*/
|
||||
window.rustCoreSrpDeriveVerifier = async function(privateKey) {
|
||||
if (!await initRustCore()) {
|
||||
throw new Error('Rust WASM module not available');
|
||||
}
|
||||
|
||||
try {
|
||||
return wasmModule.srpDeriveVerifier(privateKey);
|
||||
} catch (error) {
|
||||
console.error('[RustCore] SRP derive verifier failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate client ephemeral keypair.
|
||||
* @returns {Promise<{public: string, secret: string}>} Object with public and secret hex strings.
|
||||
*/
|
||||
window.rustCoreSrpGenerateEphemeral = async function() {
|
||||
if (!await initRustCore()) {
|
||||
throw new Error('Rust WASM module not available');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = wasmModule.srpGenerateEphemeral();
|
||||
return {
|
||||
public: result.public,
|
||||
secret: result.secret
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[RustCore] SRP generate ephemeral failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Derive client session from ephemeral values.
|
||||
* @param {string} clientSecret - Client ephemeral secret (hex string).
|
||||
* @param {string} serverPublic - Server ephemeral public (hex string).
|
||||
* @param {string} salt - The salt (hex string).
|
||||
* @param {string} identity - The SRP identity.
|
||||
* @param {string} privateKey - The private key (hex string).
|
||||
* @returns {Promise<{key: string, proof: string}>} Object with session key and proof hex strings.
|
||||
*/
|
||||
window.rustCoreSrpDeriveSession = async function(clientSecret, serverPublic, salt, identity, privateKey) {
|
||||
if (!await initRustCore()) {
|
||||
throw new Error('Rust WASM module not available');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = wasmModule.srpDeriveSession(clientSecret, serverPublic, salt, identity, privateKey);
|
||||
return {
|
||||
key: result.key,
|
||||
proof: result.proof
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[RustCore] SRP derive session failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify the server's session proof (M2) on the client side.
|
||||
* @param {string} clientPublic - Client public ephemeral (A) as hex string.
|
||||
* @param {string} clientProof - Client proof (M1) as hex string.
|
||||
* @param {string} sessionKey - Session key (K) as hex string.
|
||||
* @param {string} serverProof - Server proof (M2) to verify as hex string.
|
||||
* @returns {Promise<boolean>} True if verification succeeds.
|
||||
*/
|
||||
window.rustCoreSrpVerifySession = async function(clientPublic, clientProof, sessionKey, serverProof) {
|
||||
if (!await initRustCore()) {
|
||||
throw new Error('Rust WASM module not available');
|
||||
}
|
||||
|
||||
try {
|
||||
return wasmModule.srpVerifySession(clientPublic, clientProof, sessionKey, serverProof);
|
||||
} catch (error) {
|
||||
console.error('[RustCore] SRP verify session failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
206
core/rust/Cargo.lock
generated
206
core/rust/Cargo.lock
generated
@@ -8,9 +8,16 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"console_error_panic_hook",
|
||||
"digest",
|
||||
"getrandom",
|
||||
"num-bigint",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"srp",
|
||||
"subtle",
|
||||
"thiserror",
|
||||
"uniffi",
|
||||
"wasm-bindgen",
|
||||
@@ -137,6 +144,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
@@ -253,6 +269,35 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs-err"
|
||||
version = "2.11.0"
|
||||
@@ -262,6 +307,29 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
@@ -307,6 +375,18 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.180"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
@@ -351,6 +431,25 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -384,6 +483,15 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.103"
|
||||
@@ -402,6 +510,36 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
@@ -498,6 +636,17 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.11"
|
||||
@@ -510,6 +659,19 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
|
||||
|
||||
[[package]]
|
||||
name = "srp"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cdd40049b29ab1315ccba7ffcf1eece8162982eab14edfac3cf2c2ffab18193"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"generic-array",
|
||||
"lazy_static",
|
||||
"num-bigint",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
@@ -522,6 +684,12 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.111"
|
||||
@@ -571,6 +739,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.8.1"
|
||||
@@ -719,6 +893,18 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.106"
|
||||
@@ -787,3 +973,23 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
@@ -36,6 +36,15 @@ chrono = { version = "0.4", default-features = false, features = ["serde", "now"
|
||||
# Error handling
|
||||
thiserror = "1.0"
|
||||
|
||||
# SRP (Secure Remote Password) protocol
|
||||
srp = "0.6"
|
||||
sha2 = "0.10"
|
||||
rand = { version = "0.8", features = ["std", "std_rng"] }
|
||||
num-bigint = "0.4"
|
||||
digest = "0.10"
|
||||
subtle = "2.5"
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
|
||||
# UniFFI for Swift/Kotlin bindings (optional)
|
||||
# Note: Don't add "cli" feature here - it pulls in heavy bindgen dependencies
|
||||
# The uniffi-cli feature enables CLI separately for the uniffi-bindgen binary only
|
||||
|
||||
@@ -179,6 +179,283 @@ fn create_error_response(message: &str) -> *mut c_char {
|
||||
string_to_c_char(error_json)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// SRP (Secure Remote Password) FFI Functions
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Generate a cryptographic salt for SRP.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - The returned pointer must be freed by calling `free_string`
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A null-terminated C string containing the salt as uppercase hex.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn srp_generate_salt_ffi() -> *mut c_char {
|
||||
string_to_c_char(crate::srp::srp_generate_salt())
|
||||
}
|
||||
|
||||
/// Derive the SRP private key from credentials.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - All input pointers must be valid null-terminated C strings
|
||||
/// - The returned pointer must be freed by calling `free_string`
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A null-terminated C string containing the private key as uppercase hex,
|
||||
/// or an error JSON if the inputs are invalid.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn srp_derive_private_key_ffi(
|
||||
salt: *const c_char,
|
||||
identity: *const c_char,
|
||||
password_hash: *const c_char,
|
||||
) -> *mut c_char {
|
||||
if salt.is_null() || identity.is_null() || password_hash.is_null() {
|
||||
return create_error_response("Null pointer argument");
|
||||
}
|
||||
|
||||
let salt_str = match CStr::from_ptr(salt).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return create_error_response("Invalid UTF-8 in salt"),
|
||||
};
|
||||
|
||||
let identity_str = match CStr::from_ptr(identity).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return create_error_response("Invalid UTF-8 in identity"),
|
||||
};
|
||||
|
||||
let password_hash_str = match CStr::from_ptr(password_hash).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return create_error_response("Invalid UTF-8 in password_hash"),
|
||||
};
|
||||
|
||||
match crate::srp::srp_derive_private_key(salt_str, identity_str, password_hash_str) {
|
||||
Ok(key) => string_to_c_char(key),
|
||||
Err(e) => create_error_response(&format!("SRP error: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive the SRP verifier from a private key.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - `private_key` must be a valid null-terminated C string
|
||||
/// - The returned pointer must be freed by calling `free_string`
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A null-terminated C string containing the verifier as uppercase hex,
|
||||
/// or an error JSON if the input is invalid.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn srp_derive_verifier_ffi(private_key: *const c_char) -> *mut c_char {
|
||||
if private_key.is_null() {
|
||||
return create_error_response("Null pointer argument");
|
||||
}
|
||||
|
||||
let private_key_str = match CStr::from_ptr(private_key).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return create_error_response("Invalid UTF-8 in private_key"),
|
||||
};
|
||||
|
||||
match crate::srp::srp_derive_verifier(private_key_str) {
|
||||
Ok(verifier) => string_to_c_char(verifier),
|
||||
Err(e) => create_error_response(&format!("SRP error: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a client ephemeral key pair.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - The returned pointer must be freed by calling `free_string`
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A null-terminated C string containing JSON: {"public": "...", "secret": "..."}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn srp_generate_ephemeral_ffi() -> *mut c_char {
|
||||
let ephemeral = crate::srp::srp_generate_ephemeral();
|
||||
match serde_json::to_string(&ephemeral) {
|
||||
Ok(json) => string_to_c_char(json),
|
||||
Err(e) => create_error_response(&format!("Failed to serialize ephemeral: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive the client session from server response.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - All input pointers must be valid null-terminated C strings
|
||||
/// - The returned pointer must be freed by calling `free_string`
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A null-terminated C string containing JSON: {"proof": "...", "key": "..."}
|
||||
/// or an error JSON if inputs are invalid.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn srp_derive_session_ffi(
|
||||
client_secret: *const c_char,
|
||||
server_public: *const c_char,
|
||||
salt: *const c_char,
|
||||
identity: *const c_char,
|
||||
private_key: *const c_char,
|
||||
) -> *mut c_char {
|
||||
if client_secret.is_null() || server_public.is_null() || salt.is_null()
|
||||
|| identity.is_null() || private_key.is_null()
|
||||
{
|
||||
return create_error_response("Null pointer argument");
|
||||
}
|
||||
|
||||
let client_secret_str = match CStr::from_ptr(client_secret).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return create_error_response("Invalid UTF-8 in client_secret"),
|
||||
};
|
||||
|
||||
let server_public_str = match CStr::from_ptr(server_public).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return create_error_response("Invalid UTF-8 in server_public"),
|
||||
};
|
||||
|
||||
let salt_str = match CStr::from_ptr(salt).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return create_error_response("Invalid UTF-8 in salt"),
|
||||
};
|
||||
|
||||
let identity_str = match CStr::from_ptr(identity).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return create_error_response("Invalid UTF-8 in identity"),
|
||||
};
|
||||
|
||||
let private_key_str = match CStr::from_ptr(private_key).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return create_error_response("Invalid UTF-8 in private_key"),
|
||||
};
|
||||
|
||||
match crate::srp::srp_derive_session(
|
||||
client_secret_str,
|
||||
server_public_str,
|
||||
salt_str,
|
||||
identity_str,
|
||||
private_key_str,
|
||||
) {
|
||||
Ok(session) => match serde_json::to_string(&session) {
|
||||
Ok(json) => string_to_c_char(json),
|
||||
Err(e) => create_error_response(&format!("Failed to serialize session: {}", e)),
|
||||
},
|
||||
Err(e) => create_error_response(&format!("SRP error: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a server ephemeral key pair.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - `verifier` must be a valid null-terminated C string
|
||||
/// - The returned pointer must be freed by calling `free_string`
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A null-terminated C string containing JSON: {"public": "...", "secret": "..."}
|
||||
/// or an error JSON if the input is invalid.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn srp_generate_ephemeral_server_ffi(verifier: *const c_char) -> *mut c_char {
|
||||
if verifier.is_null() {
|
||||
return create_error_response("Null pointer argument");
|
||||
}
|
||||
|
||||
let verifier_str = match CStr::from_ptr(verifier).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return create_error_response("Invalid UTF-8 in verifier"),
|
||||
};
|
||||
|
||||
match crate::srp::srp_generate_ephemeral_server(verifier_str) {
|
||||
Ok(ephemeral) => match serde_json::to_string(&ephemeral) {
|
||||
Ok(json) => string_to_c_char(json),
|
||||
Err(e) => create_error_response(&format!("Failed to serialize ephemeral: {}", e)),
|
||||
},
|
||||
Err(e) => create_error_response(&format!("SRP error: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive and verify the server session from client response.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - All input pointers must be valid null-terminated C strings
|
||||
/// - The returned pointer must be freed by calling `free_string`
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A null-terminated C string containing JSON:
|
||||
/// - {"proof": "...", "key": "..."} if client proof is valid
|
||||
/// - "null" if client proof is invalid (authentication failed)
|
||||
/// - Error JSON if inputs are invalid
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn srp_derive_session_server_ffi(
|
||||
server_secret: *const c_char,
|
||||
client_public: *const c_char,
|
||||
salt: *const c_char,
|
||||
identity: *const c_char,
|
||||
verifier: *const c_char,
|
||||
client_proof: *const c_char,
|
||||
) -> *mut c_char {
|
||||
if server_secret.is_null() || client_public.is_null() || salt.is_null()
|
||||
|| identity.is_null() || verifier.is_null() || client_proof.is_null()
|
||||
{
|
||||
return create_error_response("Null pointer argument");
|
||||
}
|
||||
|
||||
let server_secret_str = match CStr::from_ptr(server_secret).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return create_error_response("Invalid UTF-8 in server_secret"),
|
||||
};
|
||||
|
||||
let client_public_str = match CStr::from_ptr(client_public).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return create_error_response("Invalid UTF-8 in client_public"),
|
||||
};
|
||||
|
||||
let salt_str = match CStr::from_ptr(salt).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return create_error_response("Invalid UTF-8 in salt"),
|
||||
};
|
||||
|
||||
let identity_str = match CStr::from_ptr(identity).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return create_error_response("Invalid UTF-8 in identity"),
|
||||
};
|
||||
|
||||
let verifier_str = match CStr::from_ptr(verifier).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return create_error_response("Invalid UTF-8 in verifier"),
|
||||
};
|
||||
|
||||
let client_proof_str = match CStr::from_ptr(client_proof).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return create_error_response("Invalid UTF-8 in client_proof"),
|
||||
};
|
||||
|
||||
match crate::srp::srp_derive_session_server(
|
||||
server_secret_str,
|
||||
client_public_str,
|
||||
salt_str,
|
||||
identity_str,
|
||||
verifier_str,
|
||||
client_proof_str,
|
||||
) {
|
||||
Ok(Some(session)) => match serde_json::to_string(&session) {
|
||||
Ok(json) => string_to_c_char(json),
|
||||
Err(e) => create_error_response(&format!("Failed to serialize session: {}", e)),
|
||||
},
|
||||
Ok(None) => string_to_c_char("null".to_string()),
|
||||
Err(e) => create_error_response(&format!("SRP error: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
//! - **vault_merge**: Vault merge using Last-Write-Wins (LWW) strategy
|
||||
//! - **vault_pruner**: Prunes expired items from trash (30-day retention)
|
||||
//! - **credential_matcher**: Cross-platform credential filtering for autofill
|
||||
//! - **srp**: Secure Remote Password (SRP-6a) protocol for authentication
|
||||
//!
|
||||
//! This library accepts data as JSON and returns results as JSON.
|
||||
//! Each platform (browser, iOS, Android, .NET) handles its own I/O
|
||||
@@ -23,12 +24,18 @@
|
||||
//! // Credential matching example
|
||||
//! let credentials = get_credentials_json();
|
||||
//! let matches = filter_credentials(credentials, "https://github.com", "GitHub");
|
||||
//!
|
||||
//! // SRP authentication example
|
||||
//! let salt = srp::srp_generate_salt();
|
||||
//! let private_key = srp::srp_derive_private_key(&salt, "user", &password_hash);
|
||||
//! let verifier = srp::srp_derive_verifier(&private_key);
|
||||
//! ```
|
||||
|
||||
pub mod error;
|
||||
pub mod vault_merge;
|
||||
pub mod vault_pruner;
|
||||
pub mod credential_matcher;
|
||||
pub mod srp;
|
||||
|
||||
pub use error::VaultError;
|
||||
pub use vault_merge::{
|
||||
@@ -42,6 +49,12 @@ pub use credential_matcher::{
|
||||
filter_credentials, extract_domain, extract_root_domain,
|
||||
AutofillMatchingMode, CredentialMatcherInput, CredentialMatcherOutput,
|
||||
};
|
||||
pub use srp::{
|
||||
srp_generate_salt, srp_derive_private_key, srp_derive_verifier,
|
||||
srp_generate_ephemeral, srp_derive_session,
|
||||
srp_generate_ephemeral_server, srp_derive_session_server,
|
||||
SrpEphemeral, SrpSession, SrpError,
|
||||
};
|
||||
|
||||
// WASM bindings
|
||||
#[cfg(feature = "wasm")]
|
||||
|
||||
768
core/rust/src/srp/mod.rs
Normal file
768
core/rust/src/srp/mod.rs
Normal file
@@ -0,0 +1,768 @@
|
||||
//! SRP (Secure Remote Password) protocol implementation.
|
||||
//!
|
||||
//! # Protocol Parameters
|
||||
//! - Group: RFC 5054 2048-bit
|
||||
//! - Hash: SHA-256
|
||||
//! - Multiplier k: Computed as `k = H(N, PAD(g))`
|
||||
//! - All values: Uppercase hex strings
|
||||
//!
|
||||
//! # Client Operations
|
||||
//! - `srp_generate_salt()` - Generate a 32-byte cryptographic salt
|
||||
//! - `srp_derive_private_key()` - Derive private key x = H(salt | H(identity | ":" | password_hash))
|
||||
//! - `srp_derive_verifier()` - Derive verifier v = g^x mod N
|
||||
//! - `srp_generate_ephemeral()` - Generate client ephemeral key pair (A, a)
|
||||
//! - `srp_derive_session()` - Derive session key and proof from server response
|
||||
//!
|
||||
//! # Server Operations
|
||||
//! - `srp_generate_ephemeral_server()` - Generate server ephemeral key pair (B, b)
|
||||
//! - `srp_derive_session_server()` - Verify client proof and derive session
|
||||
|
||||
use digest::Digest;
|
||||
use num_bigint::BigUint;
|
||||
use rand::RngCore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
use srp::client::SrpClient;
|
||||
use srp::groups::G_2048;
|
||||
use srp::server::SrpServer;
|
||||
use thiserror::Error;
|
||||
|
||||
/// SRP ephemeral key pair (public and secret values).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
|
||||
pub struct SrpEphemeral {
|
||||
/// Public ephemeral value (uppercase hex)
|
||||
pub public: String,
|
||||
/// Secret ephemeral value (uppercase hex)
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
/// SRP session containing proof and shared key.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
|
||||
pub struct SrpSession {
|
||||
/// Session proof (uppercase hex)
|
||||
pub proof: String,
|
||||
/// Shared session key (uppercase hex)
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
/// SRP-related errors.
|
||||
#[derive(Error, Debug, Clone)]
|
||||
#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
|
||||
#[cfg_attr(feature = "uniffi", uniffi(flat_error))]
|
||||
pub enum SrpError {
|
||||
#[error("Invalid hex string: {0}")]
|
||||
InvalidHex(String),
|
||||
#[error("Invalid parameter: {0}")]
|
||||
InvalidParameter(String),
|
||||
#[error("Authentication failed: {0}")]
|
||||
AuthenticationFailed(String),
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// Helper Functions
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Convert bytes to uppercase hex string.
|
||||
fn bytes_to_hex(bytes: &[u8]) -> String {
|
||||
bytes
|
||||
.iter()
|
||||
.map(|b| format!("{:02X}", b))
|
||||
.collect::<String>()
|
||||
}
|
||||
|
||||
/// Convert hex string to bytes.
|
||||
fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, SrpError> {
|
||||
// Handle both uppercase and lowercase hex
|
||||
let hex = hex.trim();
|
||||
if hex.is_empty() {
|
||||
return Err(SrpError::InvalidHex("empty hex string".to_string()));
|
||||
}
|
||||
|
||||
// Remove 0x prefix if present
|
||||
let hex = hex.strip_prefix("0x").unwrap_or(hex);
|
||||
let hex = hex.strip_prefix("0X").unwrap_or(hex);
|
||||
|
||||
if hex.len() % 2 != 0 {
|
||||
return Err(SrpError::InvalidHex(format!(
|
||||
"odd length hex string: {}",
|
||||
hex.len()
|
||||
)));
|
||||
}
|
||||
|
||||
(0..hex.len())
|
||||
.step_by(2)
|
||||
.map(|i| {
|
||||
u8::from_str_radix(&hex[i..i + 2], 16)
|
||||
.map_err(|e| SrpError::InvalidHex(format!("invalid hex at position {}: {}", i, e)))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Generate cryptographically secure random bytes.
|
||||
fn generate_random_bytes(len: usize) -> Vec<u8> {
|
||||
let mut bytes = vec![0u8; len];
|
||||
rand::thread_rng().fill_bytes(&mut bytes);
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Pad a BigUint to a specific length (for SRP compatibility).
|
||||
fn pad_to_length(bytes: Vec<u8>, target_len: usize) -> Vec<u8> {
|
||||
if bytes.len() >= target_len {
|
||||
bytes
|
||||
} else {
|
||||
let mut padded = vec![0u8; target_len - bytes.len()];
|
||||
padded.extend(bytes);
|
||||
padded
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// Client Operations
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Generate a cryptographic salt for SRP.
|
||||
///
|
||||
/// Returns a 32-byte random salt as an uppercase hex string.
|
||||
pub fn srp_generate_salt() -> String {
|
||||
let salt = generate_random_bytes(32);
|
||||
bytes_to_hex(&salt)
|
||||
}
|
||||
|
||||
/// Derive the SRP private key (x) from credentials.
|
||||
///
|
||||
/// Formula: x = H(salt | H(identity | ":" | password_hash))
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `salt` - Salt as hex string
|
||||
/// * `identity` - User identity
|
||||
/// * `password_hash` - Pre-hashed password as hex string
|
||||
///
|
||||
/// # Returns
|
||||
/// Private key as uppercase hex string
|
||||
pub fn srp_derive_private_key(
|
||||
salt: &str,
|
||||
identity: &str,
|
||||
password_hash: &str,
|
||||
) -> Result<String, SrpError> {
|
||||
let salt_bytes = hex_to_bytes(salt)?;
|
||||
|
||||
// Compute identity hash: H(identity | ":" | password_hash)
|
||||
let mut identity_hasher = Sha256::new();
|
||||
identity_hasher.update(identity.as_bytes());
|
||||
identity_hasher.update(b":");
|
||||
identity_hasher.update(password_hash.as_bytes());
|
||||
let identity_hash = identity_hasher.finalize();
|
||||
|
||||
// Compute x = H(salt | identity_hash)
|
||||
let mut x_hasher = Sha256::new();
|
||||
x_hasher.update(&salt_bytes);
|
||||
x_hasher.update(&identity_hash);
|
||||
let x = x_hasher.finalize();
|
||||
|
||||
Ok(bytes_to_hex(&x))
|
||||
}
|
||||
|
||||
/// Derive the SRP verifier (v) from a private key.
|
||||
///
|
||||
/// Formula: v = g^x mod N
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `private_key` - Private key as hex string
|
||||
///
|
||||
/// # Returns
|
||||
/// Verifier as uppercase hex string (256 bytes)
|
||||
pub fn srp_derive_verifier(private_key: &str) -> Result<String, SrpError> {
|
||||
let x_bytes = hex_to_bytes(private_key)?;
|
||||
let x = BigUint::from_bytes_be(&x_bytes);
|
||||
|
||||
// v = g^x mod N
|
||||
let v = G_2048.g.modpow(&x, &G_2048.n);
|
||||
|
||||
// Pad to N length (256 bytes for 2048-bit group)
|
||||
let v_bytes = pad_to_length(v.to_bytes_be(), 256);
|
||||
Ok(bytes_to_hex(&v_bytes))
|
||||
}
|
||||
|
||||
/// Generate a client ephemeral key pair.
|
||||
///
|
||||
/// Computes A = g^a mod N where a is a random 64-byte secret.
|
||||
pub fn srp_generate_ephemeral() -> SrpEphemeral {
|
||||
let client = SrpClient::<Sha256>::new(&G_2048);
|
||||
|
||||
// Generate 64 bytes of random data for the secret
|
||||
let a = generate_random_bytes(64);
|
||||
|
||||
// Compute public ephemeral A = g^a mod N
|
||||
let a_pub = client.compute_public_ephemeral(&a);
|
||||
|
||||
// Pad to N length (256 bytes for 2048-bit group)
|
||||
let a_pub_padded = pad_to_length(a_pub, 256);
|
||||
|
||||
SrpEphemeral {
|
||||
public: bytes_to_hex(&a_pub_padded),
|
||||
secret: bytes_to_hex(&a),
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive the client session from server response.
|
||||
///
|
||||
/// Computes the shared session key K and client proof M1.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client_secret` - Client secret ephemeral (a) as hex string
|
||||
/// * `server_public` - Server public ephemeral (B) as hex string
|
||||
/// * `salt` - Salt as hex string
|
||||
/// * `identity` - User identity
|
||||
/// * `private_key` - Private key (x) as hex string
|
||||
///
|
||||
/// # Returns
|
||||
/// Session with proof (M1) and key (K), or error if B is invalid
|
||||
pub fn srp_derive_session(
|
||||
client_secret: &str,
|
||||
server_public: &str,
|
||||
salt: &str,
|
||||
identity: &str,
|
||||
private_key: &str,
|
||||
) -> Result<SrpSession, SrpError> {
|
||||
let a = hex_to_bytes(client_secret)?;
|
||||
let b_pub = hex_to_bytes(server_public)?;
|
||||
let salt_bytes = hex_to_bytes(salt)?;
|
||||
let x_bytes = hex_to_bytes(private_key)?;
|
||||
|
||||
let client = SrpClient::<Sha256>::new(&G_2048);
|
||||
|
||||
// Convert to BigUint for calculations
|
||||
let a_big = BigUint::from_bytes_be(&a);
|
||||
let a_pub = client.compute_a_pub(&a_big);
|
||||
let b_pub_big = BigUint::from_bytes_be(&b_pub);
|
||||
|
||||
// Check for malicious B (B mod N must not be 0)
|
||||
if &b_pub_big % &G_2048.n == BigUint::default() {
|
||||
return Err(SrpError::InvalidParameter(
|
||||
"server public ephemeral is invalid".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Pad A and B to N length for hashing
|
||||
let a_pub_bytes = pad_to_length(a_pub.to_bytes_be(), 256);
|
||||
let b_pub_bytes = pad_to_length(b_pub, 256);
|
||||
|
||||
// Compute u = H(A | B)
|
||||
let u = compute_u(&a_pub_bytes, &b_pub_bytes);
|
||||
|
||||
// Compute k = H(N | g)
|
||||
let k = compute_k();
|
||||
|
||||
// x as BigUint
|
||||
let x = BigUint::from_bytes_be(&x_bytes);
|
||||
|
||||
// S = (B - k*g^x)^(a + u*x) mod N
|
||||
let kg_x = (&k * G_2048.g.modpow(&x, &G_2048.n)) % &G_2048.n;
|
||||
let base = ((&G_2048.n + &b_pub_big) - &kg_x) % &G_2048.n;
|
||||
let exp = (&u * &x) + &a_big;
|
||||
let s = base.modpow(&exp, &G_2048.n);
|
||||
|
||||
// K = H(S)
|
||||
let s_bytes = pad_to_length(s.to_bytes_be(), 256);
|
||||
let mut key_hasher = Sha256::new();
|
||||
key_hasher.update(&s_bytes);
|
||||
let key = key_hasher.finalize();
|
||||
|
||||
// M1 = H(H(N) XOR H(g) | H(I) | s | A | B | K)
|
||||
let m1 = compute_m1(&a_pub_bytes, &b_pub_bytes, &salt_bytes, identity, &key);
|
||||
|
||||
Ok(SrpSession {
|
||||
proof: bytes_to_hex(&m1),
|
||||
key: bytes_to_hex(&key),
|
||||
})
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// Server Operations
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Generate a server ephemeral key pair.
|
||||
///
|
||||
/// Computes B = k*v + g^b mod N where b is a random 64-byte secret.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `verifier` - Password verifier (v) as hex string
|
||||
pub fn srp_generate_ephemeral_server(verifier: &str) -> Result<SrpEphemeral, SrpError> {
|
||||
let v_bytes = hex_to_bytes(verifier)?;
|
||||
|
||||
let server = SrpServer::<Sha256>::new(&G_2048);
|
||||
|
||||
// Generate 64 bytes of random data for the secret
|
||||
let b = generate_random_bytes(64);
|
||||
|
||||
// Compute public ephemeral B = k*v + g^b mod N
|
||||
let b_pub = server.compute_public_ephemeral(&b, &v_bytes);
|
||||
|
||||
// Pad to N length (256 bytes for 2048-bit group)
|
||||
let b_pub_padded = pad_to_length(b_pub, 256);
|
||||
|
||||
Ok(SrpEphemeral {
|
||||
public: bytes_to_hex(&b_pub_padded),
|
||||
secret: bytes_to_hex(&b),
|
||||
})
|
||||
}
|
||||
|
||||
/// Derive and verify the server session from client response.
|
||||
///
|
||||
/// Verifies client proof M1 and computes server proof M2.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `server_secret` - Server secret ephemeral (b) as hex string
|
||||
/// * `client_public` - Client public ephemeral (A) as hex string
|
||||
/// * `salt` - Salt as hex string
|
||||
/// * `identity` - User identity
|
||||
/// * `verifier` - Password verifier (v) as hex string
|
||||
/// * `client_proof` - Client proof (M1) as hex string
|
||||
///
|
||||
/// # Returns
|
||||
/// Session with proof (M2) and key (K) if verification succeeds, None if M1 is invalid
|
||||
pub fn srp_derive_session_server(
|
||||
server_secret: &str,
|
||||
client_public: &str,
|
||||
salt: &str,
|
||||
identity: &str,
|
||||
verifier: &str,
|
||||
client_proof: &str,
|
||||
) -> Result<Option<SrpSession>, SrpError> {
|
||||
let b = hex_to_bytes(server_secret)?;
|
||||
let a_pub = hex_to_bytes(client_public)?;
|
||||
let salt_bytes = hex_to_bytes(salt)?;
|
||||
let v_bytes = hex_to_bytes(verifier)?;
|
||||
let client_m1 = hex_to_bytes(client_proof)?;
|
||||
|
||||
// Convert to BigUint for calculations
|
||||
let b_big = BigUint::from_bytes_be(&b);
|
||||
let a_pub_big = BigUint::from_bytes_be(&a_pub);
|
||||
let v = BigUint::from_bytes_be(&v_bytes);
|
||||
|
||||
// Check for malicious A (A mod N must not be 0)
|
||||
if &a_pub_big % &G_2048.n == BigUint::default() {
|
||||
return Err(SrpError::InvalidParameter(
|
||||
"client public ephemeral is invalid".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Compute k = H(N | g)
|
||||
let k = compute_k();
|
||||
|
||||
// B = k*v + g^b mod N
|
||||
let kv = (&k * &v) % &G_2048.n;
|
||||
let b_pub = (&kv + G_2048.g.modpow(&b_big, &G_2048.n)) % &G_2048.n;
|
||||
|
||||
// Pad A and B to N length
|
||||
let a_pub_bytes = pad_to_length(a_pub.clone(), 256);
|
||||
let b_pub_bytes = pad_to_length(b_pub.to_bytes_be(), 256);
|
||||
|
||||
// Compute u = H(A | B)
|
||||
let u = compute_u(&a_pub_bytes, &b_pub_bytes);
|
||||
|
||||
// S = (A * v^u)^b mod N
|
||||
let v_u = v.modpow(&u, &G_2048.n);
|
||||
let base = (&a_pub_big * &v_u) % &G_2048.n;
|
||||
let s = base.modpow(&b_big, &G_2048.n);
|
||||
|
||||
// K = H(S)
|
||||
let s_bytes = pad_to_length(s.to_bytes_be(), 256);
|
||||
let mut key_hasher = Sha256::new();
|
||||
key_hasher.update(&s_bytes);
|
||||
let key = key_hasher.finalize();
|
||||
|
||||
// M1 = H(H(N) XOR H(g) | H(I) | s | A | B | K)
|
||||
let expected_m1 = compute_m1(&a_pub_bytes, &b_pub_bytes, &salt_bytes, identity, &key);
|
||||
|
||||
// Verify client proof using constant-time comparison
|
||||
use subtle::ConstantTimeEq;
|
||||
if expected_m1.ct_eq(&client_m1).unwrap_u8() != 1 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// M2 = H(A | M1 | K)
|
||||
let m2 = compute_m2(&a_pub_bytes, &expected_m1, &key);
|
||||
|
||||
Ok(Some(SrpSession {
|
||||
proof: bytes_to_hex(&m2),
|
||||
key: bytes_to_hex(&key),
|
||||
}))
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// Internal Helper Functions
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Compute u = H(A | B)
|
||||
fn compute_u(a_pub: &[u8], b_pub: &[u8]) -> BigUint {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(a_pub);
|
||||
hasher.update(b_pub);
|
||||
BigUint::from_bytes_be(&hasher.finalize())
|
||||
}
|
||||
|
||||
/// Compute k = H(N | PAD(g))
|
||||
fn compute_k() -> BigUint {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&G_2048.n.to_bytes_be());
|
||||
// Pad g to the same length as N
|
||||
let g_padded = pad_to_length(G_2048.g.to_bytes_be(), 256);
|
||||
hasher.update(&g_padded);
|
||||
BigUint::from_bytes_be(&hasher.finalize())
|
||||
}
|
||||
|
||||
/// Compute M1 = H(H(N) XOR H(g) | H(I) | s | A | B | K)
|
||||
///
|
||||
/// Note: H(g) uses g without padding, unlike k = H(N, PAD(g))
|
||||
fn compute_m1(a_pub: &[u8], b_pub: &[u8], salt: &[u8], identity: &str, key: &[u8]) -> Vec<u8> {
|
||||
// H(N)
|
||||
let mut n_hasher = Sha256::new();
|
||||
n_hasher.update(&G_2048.n.to_bytes_be());
|
||||
let h_n = n_hasher.finalize();
|
||||
|
||||
// H(g) - NOT padded
|
||||
let mut g_hasher = Sha256::new();
|
||||
g_hasher.update(&G_2048.g.to_bytes_be());
|
||||
let h_g = g_hasher.finalize();
|
||||
|
||||
// H(N) XOR H(g)
|
||||
let h_n_xor_h_g: Vec<u8> = h_n.iter().zip(h_g.iter()).map(|(a, b)| a ^ b).collect();
|
||||
|
||||
// H(I)
|
||||
let mut i_hasher = Sha256::new();
|
||||
i_hasher.update(identity.as_bytes());
|
||||
let h_i = i_hasher.finalize();
|
||||
|
||||
// M1 = H(H(N) XOR H(g) | H(I) | s | A | B | K)
|
||||
let mut m1_hasher = Sha256::new();
|
||||
m1_hasher.update(&h_n_xor_h_g);
|
||||
m1_hasher.update(&h_i);
|
||||
m1_hasher.update(salt);
|
||||
m1_hasher.update(a_pub);
|
||||
m1_hasher.update(b_pub);
|
||||
m1_hasher.update(key);
|
||||
|
||||
m1_hasher.finalize().to_vec()
|
||||
}
|
||||
|
||||
/// Compute M2 = H(A | M1 | K)
|
||||
fn compute_m2(a_pub: &[u8], m1: &[u8], key: &[u8]) -> Vec<u8> {
|
||||
let mut m2_hasher = Sha256::new();
|
||||
m2_hasher.update(a_pub);
|
||||
m2_hasher.update(m1);
|
||||
m2_hasher.update(key);
|
||||
m2_hasher.finalize().to_vec()
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// Client Verification
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Verify the server's session proof (M2) on the client side.
|
||||
///
|
||||
/// This confirms that the server successfully derived the same session key.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client_public` - Client public ephemeral (A) as hex string
|
||||
/// * `client_proof` - Client proof (M1) as hex string
|
||||
/// * `session_key` - Session key (K) as hex string
|
||||
/// * `server_proof` - Server proof (M2) as hex string to verify
|
||||
///
|
||||
/// # Returns
|
||||
/// True if verification succeeds, false otherwise
|
||||
pub fn srp_verify_session(
|
||||
client_public: &str,
|
||||
client_proof: &str,
|
||||
session_key: &str,
|
||||
server_proof: &str,
|
||||
) -> Result<bool, SrpError> {
|
||||
let a_pub_bytes = hex_to_bytes(client_public)?;
|
||||
let m1_bytes = hex_to_bytes(client_proof)?;
|
||||
let key_bytes = hex_to_bytes(session_key)?;
|
||||
let server_m2_bytes = hex_to_bytes(server_proof)?;
|
||||
|
||||
// Compute expected M2 = H(A | M1 | K)
|
||||
let expected_m2 = compute_m2(&a_pub_bytes, &m1_bytes, &key_bytes);
|
||||
|
||||
// Constant-time comparison for security
|
||||
use subtle::ConstantTimeEq;
|
||||
Ok(expected_m2.ct_eq(&server_m2_bytes).unwrap_u8() == 1)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// Tests
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_generate_salt() {
|
||||
let salt = srp_generate_salt();
|
||||
assert_eq!(salt.len(), 64); // 32 bytes = 64 hex chars
|
||||
// Should be valid hex
|
||||
assert!(hex_to_bytes(&salt).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_derive_private_key() {
|
||||
let salt = "0A0B0C0D0E0F10111213141516171819";
|
||||
let identity = "testuser";
|
||||
let password_hash = "AABBCCDD";
|
||||
|
||||
let private_key = srp_derive_private_key(salt, identity, password_hash).unwrap();
|
||||
eprintln!("Rust Private Key: {}", private_key);
|
||||
|
||||
let expected = "ACD81DF26882B20336CF2A8CDE3CABA35BA359805FDFC4567EA7BD74E8302473";
|
||||
|
||||
// Should be 32 bytes = 64 hex chars
|
||||
assert_eq!(private_key.len(), 64);
|
||||
assert!(hex_to_bytes(&private_key).is_ok());
|
||||
|
||||
// Same inputs should produce same output
|
||||
let private_key2 = srp_derive_private_key(salt, identity, password_hash).unwrap();
|
||||
assert_eq!(private_key, private_key2);
|
||||
|
||||
assert_eq!(private_key.to_uppercase(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_derive_verifier() {
|
||||
let salt = "0A0B0C0D0E0F10111213141516171819";
|
||||
let identity = "testuser";
|
||||
let password_hash = "AABBCCDD";
|
||||
|
||||
let private_key = srp_derive_private_key(salt, identity, password_hash).unwrap();
|
||||
let verifier = srp_derive_verifier(&private_key).unwrap();
|
||||
eprintln!("Rust Verifier: {}", verifier);
|
||||
|
||||
let expected = "378FAC69B16F469FB21294F7C74429CD288F47E331E8BA02FFD7C36F2914472A9F2A8C69FFEA434C9F78FCA7E7E41CBBF591FFA589460F023EF3A6F7F6B84366458893C52F8A3304E2247C50BDAE13F4463281B8CDCC519DD563A926C93D9A33E08C1DE2EFB6102BD4BFFE97D9DA9A20354393FA041C8C0459D9D11907E11B75DE4F74990CD0364BA3884C697CF548E31707162D033576B96756A9C8B622332AC9631F62D170445CF33A5EF7E1BE82EC949A5F1FD4AAF1767EE861C729E348FD4209F552BEA5A2F059C64985F4DD2495896AE33315F54329192715AB27EA32B0AF56AC8991C9F708260EF3B5D263FA55B6380CDD294F272FFD1DD86116F0C06C";
|
||||
|
||||
// Should be 256 bytes = 512 hex chars (padded to 2048-bit group size)
|
||||
assert_eq!(verifier.len(), 512);
|
||||
assert!(hex_to_bytes(&verifier).is_ok());
|
||||
|
||||
assert_eq!(verifier.to_uppercase(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_ephemeral() {
|
||||
let ephemeral = srp_generate_ephemeral();
|
||||
|
||||
// Public should be 256 bytes = 512 hex chars
|
||||
assert_eq!(ephemeral.public.len(), 512);
|
||||
// Secret should be 64 bytes = 128 hex chars
|
||||
assert_eq!(ephemeral.secret.len(), 128);
|
||||
|
||||
// Both should be valid hex
|
||||
assert!(hex_to_bytes(&ephemeral.public).is_ok());
|
||||
assert!(hex_to_bytes(&ephemeral.secret).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_ephemeral_server() {
|
||||
// First derive a verifier
|
||||
let salt = srp_generate_salt();
|
||||
let private_key = srp_derive_private_key(&salt, "testuser", "PASSWORDHASH").unwrap();
|
||||
let verifier = srp_derive_verifier(&private_key).unwrap();
|
||||
|
||||
// Generate server ephemeral
|
||||
let ephemeral = srp_generate_ephemeral_server(&verifier).unwrap();
|
||||
|
||||
// Public should be 256 bytes = 512 hex chars
|
||||
assert_eq!(ephemeral.public.len(), 512);
|
||||
// Secret should be 64 bytes = 128 hex chars
|
||||
assert_eq!(ephemeral.secret.len(), 128);
|
||||
}
|
||||
|
||||
/// Test with fixed values for deterministic verification.
|
||||
#[test]
|
||||
fn test_fixed_values() {
|
||||
let salt = "0A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F";
|
||||
let identity = "testuser";
|
||||
let password_hash = "AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899";
|
||||
|
||||
let expected_private_key = "37D921B103087DDBCFEE50E240DBF5904BBC021BD07391F206CA74BE5430D79B";
|
||||
let expected_verifier = "603ABD0F6C5494976B140BBF29D988989FD88654438994959D851C83FC891FA22C81B7CD3B1BBC5472651473183789A4DB5454D530BDEF328DCBA19C112ED266584D8750AEFDCFC0076FD40B3E16773672994C7CB56B4F6CD5FCA47927F9688483937890054D208DDBDD5117F18461B6AD7A279495583B7D99CDC1EB678E9402171F43DC7732549B5A5A3A4A2BF586686887E09D1DED55A7945C20F4DB62915DCF7FD4D7ECED87758B3E19E25CFC668FDB92FCE15E9452DE7F78BDB9BC80DE25882769870E156B2860A169F33045298CEC7700975E3EF4AAE5B41CE6086E2593EDCF2BEA8F3B613258259197C4AE8A67055ED5546C83F6EF035BA788EC63A1AE";
|
||||
|
||||
let private_key = srp_derive_private_key(salt, identity, password_hash).unwrap();
|
||||
eprintln!("Rust Private Key: {}", private_key);
|
||||
assert_eq!(private_key.to_uppercase(), expected_private_key);
|
||||
|
||||
let verifier = srp_derive_verifier(&private_key).unwrap();
|
||||
eprintln!("Rust Verifier: {}", verifier);
|
||||
assert_eq!(verifier.to_uppercase(), expected_verifier);
|
||||
}
|
||||
|
||||
/// Test session derivation with fixed ephemeral values.
|
||||
#[test]
|
||||
fn test_session_fixed_values() {
|
||||
let salt = "0A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F";
|
||||
let identity = "testuser";
|
||||
let password_hash = "AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899";
|
||||
|
||||
let private_key = srp_derive_private_key(salt, identity, password_hash).unwrap();
|
||||
|
||||
let client_secret = "89697cc13c1cea1f44c5f6b3f8f0cb7ce28246c80de10ca5d4976575dbcb0318";
|
||||
let server_public = "523d0e314fccaace5ad5007357b07bb2fb2c5f566be0b812cbe4ffa65adc5bdd5cd59d9ca921b7491481d2963733513968e7bea637a733665f8e9fb7a18ba613a03740eed9ea3795489659a486cd87352054ed49f0636bb2605b8d836a459151cb670d35e8377202d9e1569bf88d0c86bd83d303d8775a65867b68fc7f9a9d5d59c76c413cb1b4d33f1d5eb784d1d18a5705800729a5d566548297c3b84ec1077c4546ab3c9b159a6d6c7265cdc784f36f731fa371e14bc506a544713591579d0a6952c2539746963434f0e97a024c0e93701008e4c54b620a9259d071b88c0a4cf102eaa22732ecfcd1fd23a81ee180074db1b5cee1b3e9172f76153f8d46bc";
|
||||
|
||||
let expected_session_key = "AD713F5D8F520B7B9413CDD9EF6D9B5FE37F23A9B62C5E2B90D2291F8C3A9E6F";
|
||||
let expected_session_proof = "698D0DA7137A0FC4A55B49525C1312ADCD07788E8CD5FFF5BD195B3C17B6B3DF";
|
||||
|
||||
let session = srp_derive_session(
|
||||
client_secret,
|
||||
server_public,
|
||||
salt,
|
||||
identity,
|
||||
&private_key,
|
||||
).unwrap();
|
||||
|
||||
eprintln!("Rust Session Key: {}", session.key);
|
||||
eprintln!("Rust Session Proof: {}", session.proof);
|
||||
|
||||
assert_eq!(session.key.to_uppercase(), expected_session_key);
|
||||
assert_eq!(session.proof.to_uppercase(), expected_session_proof);
|
||||
}
|
||||
|
||||
/// Test with realistic 32-byte salt.
|
||||
#[test]
|
||||
fn test_realistic_salt() {
|
||||
let salt = "7c9d6615bfeb06c552c7fbcbfbe7030035a09f058ed7cf7755ca6d3bfa56393c";
|
||||
let username = "testuser";
|
||||
let password_hash = "ABCD1234567890ABCD1234567890ABCD1234567890ABCD1234567890ABCD1234";
|
||||
|
||||
let expected_private_key = "352C41C945185EDC02EBA1087A02D06A686A194D3542AE174B4F75F340E4E02E";
|
||||
let expected_verifier = "8612168CF700A1CBAE568175B1BDD9B93874A9029B2EA34126910EABFE7DCEA57345560AD96754E1C5A5A2272F1C794D7C6A7D5A756FD37EF78170A3162051035D115AA376F85330701586A714C97413F84BAE12A87497357C0483E443B7D3B75B3C19BCF845ABD38956D2EAEFE733DC696D88277245DC7E25C9013D77053F82E9400F6918BF58176D536EB7D90572A645790E6F5660FD0FB8D5673B584F1F33F06C824CA1CF246BED84E228745CD4ABC1184E5057D03191AB9253F86A407970A4578DC6763D7D42AF2CB71C79F60BB71CA16CF98A17E4F3D62BE8396593427487115163B668A8E0069487C763342B58EFAF9499EBB87DE07E52836B3DF4F28C";
|
||||
|
||||
let private_key = srp_derive_private_key(salt, username, password_hash).unwrap();
|
||||
eprintln!("Rust Private Key: {}", private_key);
|
||||
assert_eq!(private_key.to_uppercase(), expected_private_key);
|
||||
|
||||
let verifier = srp_derive_verifier(&private_key).unwrap();
|
||||
eprintln!("Rust Verifier: {}", verifier);
|
||||
assert_eq!(verifier.to_uppercase(), expected_verifier);
|
||||
|
||||
// Test session derivation
|
||||
let client_secret = "d21695287e680db505882ba699bb1a417fe064cc817ead8f2e872fb4b8612273";
|
||||
let server_public = "02ea98a39b29fee876b183124e9dd8f4e5dedf429a1bb0e74dafd67a6a855f8e43a317edb17b93fc6c42c7ed5a2d5cc166fe9dabc66e71475a3a947aec440c23e5c8b347ee4352a84a2fb94d683d1545ef2ac7571e5032d68a0bdfe8cc16d8cf852851dc9a74690d35439a722dc22eaa682ee50eb354131445fd414d4e30dd7653560a4342ffccf392f4b658b37f939a179f01be15aa4364f7d720eebb850a5cad023ce07ed09f47da00ba00ac31df2bb251c2e910a8d50044b9dc926711b648718357da4b233078a17862e5ad57df0cb13325ef39acd42625fd858f0073e073bd61eee07a89be4c2d4b52d868324fea7b68acf3dce94733973469fdc1cc8d32";
|
||||
|
||||
let expected_session_key = "7564C550D5BF148D17B33C251B71EA2E0CD96D70E207B58622D9FF78BEE609A4";
|
||||
let expected_session_proof = "87BF2829F780EF88C1BFB63F39547DAA3CC787B40978C27CDC50FDEBFD324470";
|
||||
|
||||
let session = srp_derive_session(
|
||||
client_secret,
|
||||
server_public,
|
||||
salt,
|
||||
username,
|
||||
&private_key,
|
||||
).unwrap();
|
||||
|
||||
eprintln!("Rust Session Key: {}", session.key);
|
||||
eprintln!("Rust Session Proof: {}", session.proof);
|
||||
|
||||
assert_eq!(session.key.to_uppercase(), expected_session_key);
|
||||
assert_eq!(session.proof.to_uppercase(), expected_session_proof);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_srp_flow() {
|
||||
// 1. Registration: Generate salt and verifier
|
||||
let salt = srp_generate_salt();
|
||||
let identity = "testuser@example.com";
|
||||
let password_hash = "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF";
|
||||
|
||||
let private_key = srp_derive_private_key(&salt, identity, password_hash).unwrap();
|
||||
let verifier = srp_derive_verifier(&private_key).unwrap();
|
||||
|
||||
// 2. Login: Client generates ephemeral
|
||||
let client_ephemeral = srp_generate_ephemeral();
|
||||
|
||||
// 3. Server generates ephemeral and sends to client
|
||||
let server_ephemeral = srp_generate_ephemeral_server(&verifier).unwrap();
|
||||
|
||||
// 4. Client derives session
|
||||
let client_session = srp_derive_session(
|
||||
&client_ephemeral.secret,
|
||||
&server_ephemeral.public,
|
||||
&salt,
|
||||
identity,
|
||||
&private_key,
|
||||
).unwrap();
|
||||
|
||||
// 5. Server verifies client proof and derives session
|
||||
let server_session = srp_derive_session_server(
|
||||
&server_ephemeral.secret,
|
||||
&client_ephemeral.public,
|
||||
&salt,
|
||||
identity,
|
||||
&verifier,
|
||||
&client_session.proof,
|
||||
).unwrap();
|
||||
|
||||
// Server should successfully verify and return a session
|
||||
assert!(server_session.is_some());
|
||||
let server_session = server_session.unwrap();
|
||||
|
||||
// Both should have the same session key
|
||||
assert_eq!(client_session.key, server_session.key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrong_password_fails() {
|
||||
// Setup with correct credentials
|
||||
let salt = srp_generate_salt();
|
||||
let identity = "testuser";
|
||||
let correct_password_hash = "CORRECT_PASSWORD_HASH_0123456789";
|
||||
let wrong_password_hash = "WRONG_PASSWORD_HASH_0123456789AB";
|
||||
|
||||
let correct_private_key = srp_derive_private_key(&salt, identity, correct_password_hash).unwrap();
|
||||
let verifier = srp_derive_verifier(&correct_private_key).unwrap();
|
||||
|
||||
// Client uses wrong password
|
||||
let wrong_private_key = srp_derive_private_key(&salt, identity, wrong_password_hash).unwrap();
|
||||
|
||||
let client_ephemeral = srp_generate_ephemeral();
|
||||
let server_ephemeral = srp_generate_ephemeral_server(&verifier).unwrap();
|
||||
|
||||
// Client derives session with wrong password
|
||||
let client_session = srp_derive_session(
|
||||
&client_ephemeral.secret,
|
||||
&server_ephemeral.public,
|
||||
&salt,
|
||||
identity,
|
||||
&wrong_private_key,
|
||||
).unwrap();
|
||||
|
||||
// Server should reject the client proof
|
||||
let server_session = srp_derive_session_server(
|
||||
&server_ephemeral.secret,
|
||||
&client_ephemeral.public,
|
||||
&salt,
|
||||
identity,
|
||||
&verifier,
|
||||
&client_session.proof,
|
||||
).unwrap();
|
||||
|
||||
// Server should return None (authentication failed)
|
||||
assert!(server_session.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hex_conversion() {
|
||||
// Test round-trip
|
||||
let original = vec![0x00, 0x01, 0x0A, 0xFF, 0x10];
|
||||
let hex = bytes_to_hex(&original);
|
||||
assert_eq!(hex, "00010AFF10");
|
||||
|
||||
let decoded = hex_to_bytes(&hex).unwrap();
|
||||
assert_eq!(decoded, original);
|
||||
|
||||
// Test lowercase input
|
||||
let decoded_lower = hex_to_bytes("00010aff10").unwrap();
|
||||
assert_eq!(decoded_lower, original);
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,121 @@ pub fn extract_root_domain(domain: String) -> String {
|
||||
crate::credential_matcher::extract_root_domain(&domain)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// SRP (Secure Remote Password) Functions
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
pub use crate::srp::{SrpEphemeral, SrpSession, SrpError};
|
||||
|
||||
/// Generate a cryptographic salt for SRP.
|
||||
/// Returns a 32-byte random salt as an uppercase hex string.
|
||||
#[uniffi::export]
|
||||
pub fn srp_generate_salt() -> String {
|
||||
crate::srp::srp_generate_salt()
|
||||
}
|
||||
|
||||
/// Derive the SRP private key (x) from credentials.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `salt` - Salt as uppercase hex string
|
||||
/// * `identity` - User identity (username or SRP identity GUID)
|
||||
/// * `password_hash` - Pre-hashed password as uppercase hex string (from Argon2id)
|
||||
///
|
||||
/// # Returns
|
||||
/// Private key as uppercase hex string
|
||||
#[uniffi::export]
|
||||
pub fn srp_derive_private_key(
|
||||
salt: String,
|
||||
identity: String,
|
||||
password_hash: String,
|
||||
) -> Result<String, SrpError> {
|
||||
crate::srp::srp_derive_private_key(&salt, &identity, &password_hash)
|
||||
}
|
||||
|
||||
/// Derive the SRP verifier (v) from a private key.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `private_key` - Private key as uppercase hex string
|
||||
///
|
||||
/// # Returns
|
||||
/// Verifier as uppercase hex string (for registration)
|
||||
#[uniffi::export]
|
||||
pub fn srp_derive_verifier(private_key: String) -> Result<String, SrpError> {
|
||||
crate::srp::srp_derive_verifier(&private_key)
|
||||
}
|
||||
|
||||
/// Generate a client ephemeral key pair.
|
||||
/// Returns a pair of public (A) and secret (a) values as uppercase hex strings.
|
||||
#[uniffi::export]
|
||||
pub fn srp_generate_ephemeral() -> SrpEphemeral {
|
||||
crate::srp::srp_generate_ephemeral()
|
||||
}
|
||||
|
||||
/// Derive the client session from server response.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client_secret` - Client secret ephemeral (a) as hex string
|
||||
/// * `server_public` - Server public ephemeral (B) as hex string
|
||||
/// * `salt` - Salt as hex string
|
||||
/// * `identity` - User identity (username or SRP identity GUID)
|
||||
/// * `private_key` - Private key (x) as hex string
|
||||
///
|
||||
/// # Returns
|
||||
/// Session containing proof and key as uppercase hex strings
|
||||
#[uniffi::export]
|
||||
pub fn srp_derive_session(
|
||||
client_secret: String,
|
||||
server_public: String,
|
||||
salt: String,
|
||||
identity: String,
|
||||
private_key: String,
|
||||
) -> Result<SrpSession, SrpError> {
|
||||
crate::srp::srp_derive_session(&client_secret, &server_public, &salt, &identity, &private_key)
|
||||
}
|
||||
|
||||
/// Generate a server ephemeral key pair.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `verifier` - Password verifier (v) as hex string
|
||||
///
|
||||
/// # Returns
|
||||
/// Ephemeral containing public (B) and secret (b) as uppercase hex strings
|
||||
#[uniffi::export]
|
||||
pub fn srp_generate_ephemeral_server(verifier: String) -> Result<SrpEphemeral, SrpError> {
|
||||
crate::srp::srp_generate_ephemeral_server(&verifier)
|
||||
}
|
||||
|
||||
/// Derive and verify the server session from client response.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `server_secret` - Server secret ephemeral (b) as hex string
|
||||
/// * `client_public` - Client public ephemeral (A) as hex string
|
||||
/// * `salt` - Salt as hex string (not used in calculation, for API compatibility)
|
||||
/// * `identity` - User identity (not used in calculation, for API compatibility)
|
||||
/// * `verifier` - Password verifier (v) as hex string
|
||||
/// * `client_proof` - Client proof (M1) as hex string
|
||||
///
|
||||
/// # Returns
|
||||
/// Session with server proof and key if client proof is valid, None otherwise
|
||||
#[uniffi::export]
|
||||
pub fn srp_derive_session_server(
|
||||
server_secret: String,
|
||||
client_public: String,
|
||||
salt: String,
|
||||
identity: String,
|
||||
verifier: String,
|
||||
client_proof: String,
|
||||
) -> Result<Option<SrpSession>, SrpError> {
|
||||
crate::srp::srp_derive_session_server(
|
||||
&server_secret,
|
||||
&client_public,
|
||||
&salt,
|
||||
&identity,
|
||||
&verifier,
|
||||
&client_proof,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -132,3 +132,156 @@ pub fn extract_domain_js(url: &str) -> String {
|
||||
pub fn extract_root_domain_js(domain: &str) -> String {
|
||||
crate::credential_matcher::extract_root_domain(domain)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// SRP (Secure Remote Password) WASM Bindings
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Generate a cryptographic salt for SRP.
|
||||
/// Returns a 32-byte random salt as an uppercase hex string.
|
||||
#[wasm_bindgen(js_name = srpGenerateSalt)]
|
||||
pub fn srp_generate_salt_js() -> String {
|
||||
crate::srp::srp_generate_salt()
|
||||
}
|
||||
|
||||
/// Derive the SRP private key (x) from credentials.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `salt` - Salt as uppercase hex string
|
||||
/// * `identity` - User identity (username or SRP identity GUID)
|
||||
/// * `password_hash` - Pre-hashed password as uppercase hex string (from Argon2id)
|
||||
///
|
||||
/// # Returns
|
||||
/// Private key as uppercase hex string
|
||||
#[wasm_bindgen(js_name = srpDerivePrivateKey)]
|
||||
pub fn srp_derive_private_key_js(
|
||||
salt: &str,
|
||||
identity: &str,
|
||||
password_hash: &str,
|
||||
) -> Result<String, JsValue> {
|
||||
crate::srp::srp_derive_private_key(salt, identity, password_hash)
|
||||
.map_err(|e| JsValue::from_str(&format!("SRP error: {}", e)))
|
||||
}
|
||||
|
||||
/// Derive the SRP verifier (v) from a private key.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `private_key` - Private key as uppercase hex string
|
||||
///
|
||||
/// # Returns
|
||||
/// Verifier as uppercase hex string (for registration)
|
||||
#[wasm_bindgen(js_name = srpDeriveVerifier)]
|
||||
pub fn srp_derive_verifier_js(private_key: &str) -> Result<String, JsValue> {
|
||||
crate::srp::srp_derive_verifier(private_key)
|
||||
.map_err(|e| JsValue::from_str(&format!("SRP error: {}", e)))
|
||||
}
|
||||
|
||||
/// Generate a client ephemeral key pair.
|
||||
/// Returns a JsValue object with `public` and `secret` properties (uppercase hex strings).
|
||||
#[wasm_bindgen(js_name = srpGenerateEphemeral)]
|
||||
pub fn srp_generate_ephemeral_js() -> Result<JsValue, JsValue> {
|
||||
let ephemeral = crate::srp::srp_generate_ephemeral();
|
||||
serde_wasm_bindgen::to_value(&ephemeral)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to serialize ephemeral: {}", e)))
|
||||
}
|
||||
|
||||
/// Derive the client session from server response.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client_secret` - Client secret ephemeral (a) as hex string
|
||||
/// * `server_public` - Server public ephemeral (B) as hex string
|
||||
/// * `salt` - Salt as hex string
|
||||
/// * `identity` - User identity (username or SRP identity GUID)
|
||||
/// * `private_key` - Private key (x) as hex string
|
||||
///
|
||||
/// # Returns
|
||||
/// JsValue object with `proof` and `key` properties (uppercase hex strings)
|
||||
#[wasm_bindgen(js_name = srpDeriveSession)]
|
||||
pub fn srp_derive_session_js(
|
||||
client_secret: &str,
|
||||
server_public: &str,
|
||||
salt: &str,
|
||||
identity: &str,
|
||||
private_key: &str,
|
||||
) -> Result<JsValue, JsValue> {
|
||||
let session = crate::srp::srp_derive_session(client_secret, server_public, salt, identity, private_key)
|
||||
.map_err(|e| JsValue::from_str(&format!("SRP error: {}", e)))?;
|
||||
serde_wasm_bindgen::to_value(&session)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to serialize session: {}", e)))
|
||||
}
|
||||
|
||||
/// Generate a server ephemeral key pair.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `verifier` - Password verifier (v) as hex string
|
||||
///
|
||||
/// # Returns
|
||||
/// JsValue object with `public` and `secret` properties (uppercase hex strings)
|
||||
#[wasm_bindgen(js_name = srpGenerateEphemeralServer)]
|
||||
pub fn srp_generate_ephemeral_server_js(verifier: &str) -> Result<JsValue, JsValue> {
|
||||
let ephemeral = crate::srp::srp_generate_ephemeral_server(verifier)
|
||||
.map_err(|e| JsValue::from_str(&format!("SRP error: {}", e)))?;
|
||||
serde_wasm_bindgen::to_value(&ephemeral)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to serialize ephemeral: {}", e)))
|
||||
}
|
||||
|
||||
/// Derive and verify the server session from client response.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `server_secret` - Server secret ephemeral (b) as hex string
|
||||
/// * `client_public` - Client public ephemeral (A) as hex string
|
||||
/// * `salt` - Salt as hex string
|
||||
/// * `identity` - User identity (username or SRP identity GUID)
|
||||
/// * `verifier` - Password verifier (v) as hex string
|
||||
/// * `client_proof` - Client proof (M1) as hex string
|
||||
///
|
||||
/// # Returns
|
||||
/// JsValue: object with `proof` and `key` if valid, null if client proof is invalid
|
||||
#[wasm_bindgen(js_name = srpDeriveSessionServer)]
|
||||
pub fn srp_derive_session_server_js(
|
||||
server_secret: &str,
|
||||
client_public: &str,
|
||||
salt: &str,
|
||||
identity: &str,
|
||||
verifier: &str,
|
||||
client_proof: &str,
|
||||
) -> Result<JsValue, JsValue> {
|
||||
let session = crate::srp::srp_derive_session_server(
|
||||
server_secret,
|
||||
client_public,
|
||||
salt,
|
||||
identity,
|
||||
verifier,
|
||||
client_proof,
|
||||
)
|
||||
.map_err(|e| JsValue::from_str(&format!("SRP error: {}", e)))?;
|
||||
|
||||
match session {
|
||||
Some(s) => serde_wasm_bindgen::to_value(&s)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to serialize session: {}", e))),
|
||||
None => Ok(JsValue::NULL),
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify the server's session proof (M2) on the client side.
|
||||
///
|
||||
/// This confirms that the server successfully derived the same session key.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client_public` - Client public ephemeral (A) as hex string
|
||||
/// * `client_proof` - Client proof (M1) as hex string
|
||||
/// * `session_key` - Session key (K) as hex string
|
||||
/// * `server_proof` - Server proof (M2) as hex string to verify
|
||||
///
|
||||
/// # Returns
|
||||
/// True if verification succeeds, false otherwise
|
||||
#[wasm_bindgen(js_name = srpVerifySession)]
|
||||
pub fn srp_verify_session_wasm(
|
||||
client_public: &str,
|
||||
client_proof: &str,
|
||||
session_key: &str,
|
||||
server_proof: &str,
|
||||
) -> Result<bool, JsValue> {
|
||||
crate::srp::srp_verify_session(client_public, client_proof, session_key, server_proof)
|
||||
.map_err(|e| JsValue::from_str(&format!("SRP error: {}", e)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user