diff --git a/src/lib/open-url.ts b/src/lib/open-url.ts new file mode 100644 index 00000000..4cd1aa74 Binary files /dev/null and b/src/lib/open-url.ts differ diff --git a/src/lib/server-discovery.ts b/src/lib/server-discovery.ts index 89d5f921..2254f67e 100644 --- a/src/lib/server-discovery.ts +++ b/src/lib/server-discovery.ts @@ -12,6 +12,24 @@ import { secureFetch } from './client-cert'; export const DISCOVERY_TIMEOUT_MS = 2500; const SCHEME_RE = /^([a-z][a-z0-9+.-]*):\/\//i; + +/** Loopback, the Android emulator's host alias, RFC 1918 ranges and `.local`. */ +export function isLocalHost(url: string): boolean { + const m = /^[a-z][a-z0-9+.-]*:\/\/([^/?#:]+)/i.exec(url); + const host = (m ? m[1] : '').toLowerCase(); + if (!host) return false; + if (host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '10.0.2.2') return true; + if (host.endsWith('.local') || host.endsWith('.localhost')) return true; + const ip = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(host); + if (!ip) return false; + const [a, b] = [Number(ip[1]), Number(ip[2])]; + return a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168); +} + +/** True when a normalised server URL uses plain http. */ +export function isCleartextUrl(url: string): boolean { + return /^http:\/\//i.test(url); +} const EMAIL_RE = /^[^\s@]+@([^\s@]+\.[^\s@]+)$/; /** @@ -32,6 +50,12 @@ export function normalizeServerUrl(input: string): string | null { value = `https://${value}`; } + // Basic credentials travel in every request. Cleartext is only acceptable + // for a server on the developer's own machine / LAN; on a public host it + // would fail opaquely on Android anyway (no cleartext traffic allowed) and + // leak the password on iOS. + if (/^http:\/\//i.test(value) && !isLocalHost(value)) return null; + // Someone pasting the endpoint they found in the webmail's settings gets the // same result as someone typing the address they use in the browser. value = value diff --git a/src/lib/totp.ts b/src/lib/totp.ts index cd0d0257..b75b10e8 100644 --- a/src/lib/totp.ts +++ b/src/lib/totp.ts @@ -46,9 +46,13 @@ export interface TotpEnrolment { url: string; } -export function generateTotpEnrolment(accountLabel: string): TotpEnrolment { +export function generateTotpEnrolment(accountLabel: string, issuerOverride?: string): TotpEnrolment { const secret = base32Encode(randomBytes(SECRET_BYTES)); - const issuer = 'Stalwart'; + // The issuer is what the authenticator app shows above the code; the + // account's mail domain identifies the service far better than the + // server software's name. + const at = accountLabel.lastIndexOf('@'); + const issuer = issuerOverride || (at > 0 ? accountLabel.slice(at + 1) : 'Bulwark'); const label = accountLabel || 'account'; const params = new URLSearchParams({ secret,