mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 00:14:12 +00:00
fix: hash WKD names with WebCrypto instead of shipping a Node crypto polyfill to the browser
This commit is contained in:
@@ -9,7 +9,7 @@ vi.mock('@/lib/browser-navigation', async (orig) => ({
|
||||
apiFetch: (...args: unknown[]) => fetchMock(...args),
|
||||
}));
|
||||
|
||||
import { dispatchApiCall, PLUGIN_STORAGE_QUOTA_BYTES, safeDownloadName, doGetPublicKeyFromWKD } from '../plugin-sandbox/host-api';
|
||||
import { dispatchApiCall, PLUGIN_STORAGE_QUOTA_BYTES, safeDownloadName, doGetPublicKeyFromWKD, getWkdHash } from '../plugin-sandbox/host-api';
|
||||
|
||||
function plugin(overrides: Partial<InstalledPlugin> = {}): InstalledPlugin {
|
||||
return {
|
||||
@@ -92,3 +92,10 @@ describe('WKD lookup', () => {
|
||||
fetchSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('WKD hash', () => {
|
||||
it('matches the RFC example (Joe.Doe)', async () => {
|
||||
// draft-koch-openpgp-webkey-service: "Joe.Doe" → iy9q119eutrkn8s1mk4r39qejnbu3n5q
|
||||
expect(await getWkdHash('Joe.Doe')).toBe('iy9q119eutrkn8s1mk4r39qejnbu3n5q');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,7 +31,6 @@ import { fileStorage } from '../plugin-storage';
|
||||
import { generateUUID } from '../utils';
|
||||
import { AddressBook, ContactCard, Identity } from '../jmap/types';
|
||||
import { EncryptionAtRestConfig, PublicKeyInfo, PublicKeyInput, useAccountSecurityStore } from '@/stores/account-security-store';
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
/**
|
||||
* Methods only callable from the privileged (same-origin) tier. These expose
|
||||
@@ -976,15 +975,16 @@ function zBase32Encode(buffer: Uint8Array): string {
|
||||
* Generates the WKD hash (SHA-1 in z-base-32) from the local-part.
|
||||
* RFC 9292: Lowercase in UTF-8 before hashing.
|
||||
*/
|
||||
function getWkdHash(localPart: string): string {
|
||||
export async function getWkdHash(localPart: string): Promise<string> {
|
||||
// NFC normalization + lowercasing
|
||||
const normalizedLocal = localPart.normalize('NFC').toLowerCase();
|
||||
|
||||
// Explicit TextEncoder usage to guarantee UTF-8 bytes
|
||||
|
||||
// Explicit TextEncoder usage to guarantee UTF-8 bytes. WebCrypto, not
|
||||
// Node's crypto: importing that pulled a ~330 KB polyfill into the client.
|
||||
const utf8Bytes = new TextEncoder().encode(normalizedLocal);
|
||||
const sha1Buffer = createHash('sha1').update(utf8Bytes).digest();
|
||||
|
||||
return zBase32Encode(sha1Buffer);
|
||||
const sha1 = await crypto.subtle.digest('SHA-1', utf8Bytes);
|
||||
|
||||
return zBase32Encode(new Uint8Array(sha1));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1031,7 +1031,7 @@ export async function doGetPublicKeyFromWKD(email: string): Promise<WkdResult> {
|
||||
if (!/^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(domain)) {
|
||||
return { status: 'ERROR', error: 'Invalid email address' };
|
||||
}
|
||||
const hash = getWkdHash(localPart);
|
||||
const hash = await getWkdHash(localPart);
|
||||
|
||||
const urls = [
|
||||
`https://openpgpkey.${domain}/.well-known/openpgpkey/hu/${hash}?l=${encodeURIComponent(localPart)}`,
|
||||
|
||||
Reference in New Issue
Block a user