diff --git a/lib/__tests__/plugin-host-api-limits.test.ts b/lib/__tests__/plugin-host-api-limits.test.ts index 5952fbb49..f4a44dafe 100644 --- a/lib/__tests__/plugin-host-api-limits.test.ts +++ b/lib/__tests__/plugin-host-api-limits.test.ts @@ -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 { 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'); + }); +}); diff --git a/lib/plugin-sandbox/host-api.ts b/lib/plugin-sandbox/host-api.ts index d437bb2c7..19650ca89 100644 --- a/lib/plugin-sandbox/host-api.ts +++ b/lib/plugin-sandbox/host-api.ts @@ -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 { // 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 { 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)}`,