diff --git a/NEW/src/JdeScoping.Client/Services/CryptoService.cs b/NEW/src/JdeScoping.Client/Services/CryptoService.cs new file mode 100644 index 0000000..0c9125c --- /dev/null +++ b/NEW/src/JdeScoping.Client/Services/CryptoService.cs @@ -0,0 +1,63 @@ +using System.Net.Http.Json; +using System.Text.Json; +using JdeScoping.Core.Models.Auth; +using Microsoft.JSInterop; + +namespace JdeScoping.Client.Services; + +/// +/// Encrypts login credentials using Web Crypto API via JavaScript interop. +/// Uses RSA-OAEP with SHA-256 to encrypt credentials before transmission. +/// +public class CryptoService : ICryptoService +{ + private readonly HttpClient _httpClient; + private readonly IJSRuntime _jsRuntime; + private string? _cachedPublicKeyPem; + private readonly SemaphoreSlim _keyLock = new(1, 1); + + public CryptoService(HttpClient httpClient, IJSRuntime jsRuntime) + { + _httpClient = httpClient; + _jsRuntime = jsRuntime; + } + + /// + public async Task EncryptLoginAsync(LoginModel model) + { + var publicKeyPem = await GetOrFetchPublicKeyAsync(); + + var json = JsonSerializer.Serialize(model); + + // Use JavaScript interop to encrypt with RSA-OAEP via browser's SubtleCrypto API + var encryptedBase64 = await _jsRuntime.InvokeAsync( + "jdeScopingInterop.rsaEncrypt", + publicKeyPem, + json); + + return encryptedBase64; + } + + private async Task GetOrFetchPublicKeyAsync() + { + if (_cachedPublicKeyPem is not null) + return _cachedPublicKeyPem; + + await _keyLock.WaitAsync(); + try + { + if (_cachedPublicKeyPem is not null) + return _cachedPublicKeyPem; + + var response = await _httpClient.GetFromJsonAsync("api/auth/public-key") + ?? throw new InvalidOperationException("Failed to fetch public key from server"); + + _cachedPublicKeyPem = response.PublicKeyPem; + return _cachedPublicKeyPem; + } + finally + { + _keyLock.Release(); + } + } +} diff --git a/NEW/src/JdeScoping.Client/Services/ICryptoService.cs b/NEW/src/JdeScoping.Client/Services/ICryptoService.cs new file mode 100644 index 0000000..905b921 --- /dev/null +++ b/NEW/src/JdeScoping.Client/Services/ICryptoService.cs @@ -0,0 +1,16 @@ +using JdeScoping.Core.Models.Auth; + +namespace JdeScoping.Client.Services; + +/// +/// Service for encrypting data using server's RSA public key. +/// +public interface ICryptoService +{ + /// + /// Encrypts login credentials for transmission to server. + /// + /// Login credentials to encrypt + /// Base64-encoded encrypted data + Task EncryptLoginAsync(LoginModel model); +} diff --git a/NEW/src/JdeScoping.Client/wwwroot/js/interop.js b/NEW/src/JdeScoping.Client/wwwroot/js/interop.js index bcf2da6..0197695 100644 --- a/NEW/src/JdeScoping.Client/wwwroot/js/interop.js +++ b/NEW/src/JdeScoping.Client/wwwroot/js/interop.js @@ -66,5 +66,51 @@ window.jdeScopingInterop = { // Remove value from sessionStorage removeSessionStorage: function (key) { sessionStorage.removeItem(key); + }, + + // RSA-OAEP encryption for login credentials + // Takes a PEM-encoded public key and plaintext, returns base64-encoded ciphertext + rsaEncrypt: async function (publicKeyPem, plaintext) { + // Extract base64 content from PEM format + const pemHeader = '-----BEGIN PUBLIC KEY-----'; + const pemFooter = '-----END PUBLIC KEY-----'; + const pemContents = publicKeyPem + .replace(pemHeader, '') + .replace(pemFooter, '') + .replace(/\s/g, ''); + + // Decode base64 to binary + const binaryDer = Uint8Array.from(atob(pemContents), c => c.charCodeAt(0)); + + // Import the public key for RSA-OAEP encryption + const publicKey = await window.crypto.subtle.importKey( + 'spki', + binaryDer, + { + name: 'RSA-OAEP', + hash: 'SHA-256' + }, + false, + ['encrypt'] + ); + + // Encode plaintext to bytes + const encoder = new TextEncoder(); + const plaintextBytes = encoder.encode(plaintext); + + // Encrypt with RSA-OAEP + const ciphertext = await window.crypto.subtle.encrypt( + { name: 'RSA-OAEP' }, + publicKey, + plaintextBytes + ); + + // Convert to base64 for transmission + const ciphertextArray = new Uint8Array(ciphertext); + let binary = ''; + for (let i = 0; i < ciphertextArray.length; i++) { + binary += String.fromCharCode(ciphertextArray[i]); + } + return btoa(binary); } };