Tweak HTTPS required message, tweak crypto.js error handling (#1181)

This commit is contained in:
Leendert de Borst
2025-09-07 12:00:45 +02:00
parent b603a177e2
commit 864a7630d5
6 changed files with 100 additions and 75 deletions

View File

@@ -30,6 +30,22 @@
<p class="text-lg text-gray-600 dark:text-gray-300 mb-8">
@Localizer["TaglineText"]
</p>
@if (_isHttpWarning)
{
<div class="bg-orange-100 border-l-4 border-orange-500 text-orange-700 p-4 mb-6" role="alert">
<div class="flex">
<div class="py-1">
<svg class="fill-current h-6 w-6 text-orange-500 mr-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M2.93 17.07A10 10 0 1 1 17.07 2.93 10 10 0 0 1 2.93 17.07zm12.73-1.41A8 8 0 1 0 4.34 4.34a8 8 0 0 0 11.32 11.32zM9 11V9h2v6H9v-4zm0-6h2v2H9V5z"/>
</svg>
</div>
<div>
<p class="font-bold">@Localizer["HttpsWarningTitle"]</p>
<p class="text-sm">@Localizer["HttpsWarningMessage"]</p>
</div>
</div>
</div>
}
<div class="space-y-4">
@if (Config.PublicRegistrationEnabled)
{
@@ -50,6 +66,7 @@
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Pages.Auth.Start", "AliasVault.Client");
private bool _isHttpWarning = false;
/// <inheritdoc />
protected override async Task OnInitializedAsync()
@@ -62,5 +79,20 @@
// Already authenticated, redirect to home page.
NavigationManager.NavigateTo("/");
}
CheckHttpProtocol();
}
/// <summary>
/// Checks if the current URL is using HTTP and shows warning if needed.
/// Only shows warning for non-localhost hostnames since browsers allow crypto operations on localhost via HTTP.
/// </summary>
private void CheckHttpProtocol()
{
var uri = new Uri(NavigationManager.Uri);
var isLocalhost = uri.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase) ||
uri.Host.Equals("127.0.0.1", StringComparison.OrdinalIgnoreCase) ||
uri.Host.Equals("::1", StringComparison.OrdinalIgnoreCase);
_isHttpWarning = uri.Scheme == "http" && !isLocalhost;
}
}

View File

@@ -4,31 +4,7 @@
@using Microsoft.Extensions.Localization
@implements IDisposable
@if (_isHttpWarning)
{
<div class="fixed bottom-0 left-0 right-0 z-50 bg-orange-500 text-white px-4 py-3">
<div class="container mx-auto">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
</svg>
<div>
<p class="font-medium">@Localizer["HttpsWarningTitle"]</p>
<p class="text-sm">@Localizer["HttpsWarningMessage"]</p>
</div>
</div>
<button @onclick="DismissHttpWarning" class="text-white hover:text-gray-200 ml-4">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</button>
</div>
</div>
</div>
}
<footer class="relative -z-10 lg:fixed bottom-0 left-0 right-0 dark:bg-gray-900 @(ShowBorder ? "border-t border-gray-200 dark:border-gray-700" : "") @(_isHttpWarning ? "mb-20" : "")">
<footer class="relative -z-10 lg:fixed bottom-0 left-0 right-0 dark:bg-gray-900 @(ShowBorder ? "border-t border-gray-200 dark:border-gray-700" : "")">
<div class="container mx-auto px-4 py-4">
<div class="flex flex-col lg:flex-row justify-between items-center">
<p class="text-sm text-center text-gray-500 mb-4 lg:mb-0">
@@ -62,14 +38,11 @@
];
private string _randomQuote = string.Empty;
private bool _isHttpWarning = false;
private bool _httpWarningDismissed = false;
/// <inheritdoc />
public void Dispose()
{
NavigationManager.LocationChanged -= RefreshQuote;
NavigationManager.LocationChanged -= CheckHttpProtocol;
}
/// <inheritdoc />
@@ -79,8 +52,6 @@
{
_randomQuote = Quotes[Random.Shared.Next(Quotes.Length)];
NavigationManager.LocationChanged += RefreshQuote;
NavigationManager.LocationChanged += CheckHttpProtocol;
CheckHttpProtocol(null, null);
}
}
@@ -92,28 +63,4 @@
_randomQuote = Quotes[Random.Shared.Next(Quotes.Length)];
StateHasChanged();
}
/// <summary>
/// Checks if the current URL is using HTTP and shows warning if needed.
/// Only shows warning for non-localhost hostnames since browsers allow crypto operations on localhost via HTTP.
/// </summary>
private void CheckHttpProtocol(object? sender, LocationChangedEventArgs? e)
{
var uri = new Uri(NavigationManager.Uri);
var isLocalhost = uri.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase) ||
uri.Host.Equals("127.0.0.1", StringComparison.OrdinalIgnoreCase) ||
uri.Host.Equals("::1", StringComparison.OrdinalIgnoreCase);
_isHttpWarning = !_httpWarningDismissed && uri.Scheme == "http" && !isLocalhost;
StateHasChanged();
}
/// <summary>
/// Dismisses the HTTP warning.
/// </summary>
private void DismissHttpWarning()
{
_httpWarningDismissed = true;
_isHttpWarning = false;
StateHasChanged();
}
}

View File

@@ -78,12 +78,4 @@
<value>Tip: Use the g+l (go lock) keyboard shortcut to lock the vault.</value>
<comment>Tip about keyboard shortcut for locking vault</comment>
</data>
<data name="HttpsWarningTitle" xml:space="preserve">
<value>HTTPS Required</value>
<comment>Title for HTTPS warning banner</comment>
</data>
<data name="HttpsWarningMessage" xml:space="preserve">
<value>Browsers only allow secure crypto operations via HTTPS, except for localhost. Login/registration won't work over HTTP with the current hostname. Please switch to HTTPS.</value>
<comment>Message explaining why HTTPS is required</comment>
</data>
</root>

View File

@@ -74,4 +74,12 @@
<value>Log in with existing account</value>
<comment>Button text for logging in with existing account</comment>
</data>
<data name="HttpsWarningTitle" xml:space="preserve">
<value>HTTPS Required</value>
<comment>Title for HTTPS warning banner</comment>
</data>
<data name="HttpsWarningMessage" xml:space="preserve">
<value>Browsers only allow secure crypto operations via HTTPS, except for localhost. Login/registration won't work over HTTP with the current hostname. Please switch to HTTPS.</value>
<comment>Message explaining why HTTPS is required</comment>
</data>
</root>

View File

@@ -866,14 +866,6 @@ video {
margin-top: 2rem;
}
.mb-20 {
margin-bottom: 5rem;
}
.ml-4 {
margin-left: 1rem;
}
.block {
display: block;
}
@@ -1533,6 +1525,11 @@ video {
border-color: rgb(254 215 170 / var(--tw-border-opacity));
}
.border-orange-500 {
--tw-border-opacity: 1;
border-color: rgb(249 115 22 / var(--tw-border-opacity));
}
.border-primary-100 {
--tw-border-opacity: 1;
border-color: rgb(253 222 133 / var(--tw-border-opacity));
@@ -1673,6 +1670,11 @@ video {
background-color: rgb(238 242 255 / var(--tw-bg-opacity));
}
.bg-orange-100 {
--tw-bg-opacity: 1;
background-color: rgb(255 237 213 / var(--tw-bg-opacity));
}
.bg-orange-50 {
--tw-bg-opacity: 1;
background-color: rgb(255 247 237 / var(--tw-bg-opacity));
@@ -1779,6 +1781,10 @@ video {
--tw-gradient-to: #d68338 var(--tw-gradient-to-position);
}
.fill-current {
fill: currentColor;
}
.fill-primary-600 {
fill: #d68338;
}
@@ -2154,6 +2160,16 @@ video {
color: rgb(67 56 202 / var(--tw-text-opacity));
}
.text-orange-500 {
--tw-text-opacity: 1;
color: rgb(249 115 22 / var(--tw-text-opacity));
}
.text-orange-700 {
--tw-text-opacity: 1;
color: rgb(194 65 12 / var(--tw-text-opacity));
}
.text-orange-800 {
--tw-text-opacity: 1;
color: rgb(154 52 18 / var(--tw-text-opacity));
@@ -2558,11 +2574,6 @@ video {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.hover\:text-gray-200:hover {
--tw-text-opacity: 1;
color: rgb(229 231 235 / var(--tw-text-opacity));
}
.hover\:underline:hover {
text-decoration-line: underline;
}

View File

@@ -1,9 +1,34 @@
/**
* Custom error class for crypto availability issues
*/
class CryptoNotAvailableError extends Error {
constructor(message) {
super(message);
this.name = 'CryptoNotAvailableError';
// Prevent stack trace from being captured
this.stack = '';
}
}
/**
* Check if crypto API is available and throw user-friendly error if not.
*/
function checkCryptoAvailable() {
if (!window.crypto || !window.crypto.subtle) {
const error = new CryptoNotAvailableError("Cryptographic operations are not available. Please ensure you are accessing AliasVault over HTTPS, as this is required for security features to work properly.");
console.error(error.message);
throw error;
}
}
/**
* AES (symmetric) encryption and decryption functions.
* @type {{encrypt: (function(*, *): Promise<string>), decrypt: (function(*, *): Promise<string>), decryptBytes: (function(*, *): Promise<Uint8Array>)}}
*/
window.cryptoInterop = {
encrypt: async function (plaintext, base64Key) {
checkCryptoAvailable();
const key = await window.crypto.subtle.importKey(
"raw",
Uint8Array.from(atob(base64Key), c => c.charCodeAt(0)),
@@ -36,6 +61,8 @@ window.cryptoInterop = {
);
},
decrypt: async function (base64Ciphertext, base64Key) {
checkCryptoAvailable();
const key = await window.crypto.subtle.importKey(
"raw",
Uint8Array.from(atob(base64Key), c => c.charCodeAt(0)),
@@ -61,6 +88,8 @@ window.cryptoInterop = {
return decoder.decode(decrypted);
},
decryptBytes: async function (base64Ciphertext, base64Key) {
checkCryptoAvailable();
const key = await window.crypto.subtle.importKey(
"raw",
Uint8Array.from(atob(base64Key), c => c.charCodeAt(0)),
@@ -96,6 +125,8 @@ window.rsaInterop = {
* @returns {Promise<{publicKey: string, privateKey: string}>} A promise that resolves to an object containing the public and private keys as JWK strings.
*/
generateRsaKeyPair : async function() {
checkCryptoAvailable();
const keyPair = await window.crypto.subtle.generateKey(
{
name: "RSA-OAEP",
@@ -122,6 +153,8 @@ window.rsaInterop = {
* @returns {Promise<string>} A promise that resolves to the encrypted data as a base64-encoded string.
*/
encryptWithPublicKey : async function(plaintext, publicKey) {
checkCryptoAvailable();
const publicKeyObj = await window.crypto.subtle.importKey(
"jwk",
JSON.parse(publicKey),
@@ -151,6 +184,8 @@ window.rsaInterop = {
* @returns {Promise<Uint8Array>} A promise that resolves to the decrypted data as a Uint8Array.
*/
decryptWithPrivateKey: async function(ciphertext, privateKey) {
checkCryptoAvailable();
try {
// Parse the private key
let parsedPrivateKey = JSON.parse(privateKey);